From d15253415a80e6d718b37319797356d8fe42ef1c Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Mon, 29 Sep 2025 09:52:35 -0700 Subject: [PATCH 01/23] fix clear-to-end being emitted at the end of a row (#4447) This was causing glitchy behavior when a line in the input was the exact width of the terminal. --- codex-rs/tui/src/custom_terminal.rs | 47 ++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/codex-rs/tui/src/custom_terminal.rs b/codex-rs/tui/src/custom_terminal.rs index 3eddbec561..aa79e980c1 100644 --- a/codex-rs/tui/src/custom_terminal.rs +++ b/codex-rs/tui/src/custom_terminal.rs @@ -412,11 +412,13 @@ fn diff_buffers<'a>(a: &'a Buffer, b: &'a Buffer) -> Vec> { .unwrap_or(0); last_nonblank_column[y as usize] = x as u16; let (x_abs, y_abs) = a.pos_of(row_start + x + 1); - updates.push(DrawCommand::ClearToEnd { - x: x_abs, - y: y_abs, - bg, - }); + if x < (a.area.width as usize).saturating_sub(1) { + updates.push(DrawCommand::ClearToEnd { + x: x_abs, + y: y_abs, + bg, + }); + } } // Cells invalidated by drawing/replacing preceding multi-width characters: @@ -570,3 +572,38 @@ impl ModifierDiff { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use ratatui::layout::Rect; + + #[test] + fn diff_buffers_does_not_emit_clear_to_end_for_full_width_row() { + let area = Rect::new(0, 0, 3, 2); + let previous = Buffer::empty(area); + let mut next = Buffer::empty(area); + + next.cell_mut((2, 0)) + .expect("cell should exist") + .set_symbol("X"); + + let commands = diff_buffers(&previous, &next); + + let clear_count = commands + .iter() + .filter(|command| matches!(command, DrawCommand::ClearToEnd { y, .. } if *y == 0)) + .count(); + assert_eq!( + 0, clear_count, + "expected diff_buffers not to emit ClearToEnd; commands: {commands:?}", + ); + assert!( + commands + .iter() + .any(|command| matches!(command, DrawCommand::Put { x: 2, y: 0, .. })), + "expected diff_buffers to update the final cell; commands: {commands:?}", + ); + } +} From 04c1782e52428b2460c90e3a82b792fcf101763a Mon Sep 17 00:00:00 2001 From: vishnu-oai Date: Mon, 29 Sep 2025 19:30:55 +0100 Subject: [PATCH 02/23] OpenTelemetry events (#2103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Title ## otel Codex can emit [OpenTelemetry](https://opentelemetry.io/) **log events** that describe each run: outbound API requests, streamed responses, user input, tool-approval decisions, and the result of every tool invocation. Export is **disabled by default** so local runs remain self-contained. Opt in by adding an `[otel]` table and choosing an exporter. ```toml [otel] environment = "staging" # defaults to "dev" exporter = "none" # defaults to "none"; set to otlp-http or otlp-grpc to send events log_user_prompt = false # defaults to false; redact prompt text unless explicitly enabled ``` Codex tags every exported event with `service.name = "codex-cli"`, the CLI version, and an `env` attribute so downstream collectors can distinguish dev/staging/prod traffic. Only telemetry produced inside the `codex_otel` crate—the events listed below—is forwarded to the exporter. ### Event catalog Every event shares a common set of metadata fields: `event.timestamp`, `conversation.id`, `app.version`, `auth_mode` (when available), `user.account_id` (when available), `terminal.type`, `model`, and `slug`. With OTEL enabled Codex emits the following event types (in addition to the metadata above): - `codex.api_request` - `cf_ray` (optional) - `attempt` - `duration_ms` - `http.response.status_code` (optional) - `error.message` (failures) - `codex.sse_event` - `event.kind` - `duration_ms` - `error.message` (failures) - `input_token_count` (completion only) - `output_token_count` (completion only) - `cached_token_count` (completion only, optional) - `reasoning_token_count` (completion only, optional) - `tool_token_count` (completion only) - `codex.user_prompt` - `prompt_length` - `prompt` (redacted unless `log_user_prompt = true`) - `codex.tool_decision` - `tool_name` - `call_id` - `decision` (`approved`, `approved_for_session`, `denied`, or `abort`) - `source` (`config` or `user`) - `codex.tool_result` - `tool_name` - `call_id` - `arguments` - `duration_ms` (execution time for the tool) - `success` (`"true"` or `"false"`) - `output` ### Choosing an exporter Set `otel.exporter` to control where events go: - `none` – leaves instrumentation active but skips exporting. This is the default. - `otlp-http` – posts OTLP log records to an OTLP/HTTP collector. Specify the endpoint, protocol, and headers your collector expects: ```toml [otel] exporter = { otlp-http = { endpoint = "https://otel.example.com/v1/logs", protocol = "binary", headers = { "x-otlp-api-key" = "${OTLP_TOKEN}" } }} ``` - `otlp-grpc` – streams OTLP log records over gRPC. Provide the endpoint and any metadata headers: ```toml [otel] exporter = { otlp-grpc = { endpoint = "https://otel.example.com:4317", headers = { "x-otlp-meta" = "abc123" } }} ``` If the exporter is `none` nothing is written anywhere; otherwise you must run or point to your own collector. All exporters run on a background batch worker that is flushed on shutdown. If you build Codex from source the OTEL crate is still behind an `otel` feature flag; the official prebuilt binaries ship with the feature enabled. When the feature is disabled the telemetry hooks become no-ops so the CLI continues to function without the extra dependencies. --------- Co-authored-by: Anton Panasenko --- codex-rs/Cargo.lock | 293 +++- codex-rs/Cargo.toml | 9 + codex-rs/core/Cargo.toml | 2 + codex-rs/core/src/apply_patch.rs | 13 +- codex-rs/core/src/chat_completions.rs | 45 +- codex-rs/core/src/client.rs | 113 +- codex-rs/core/src/codex.rs | 258 +++- codex-rs/core/src/config.rs | 27 + codex-rs/core/src/config_types.rs | 60 + codex-rs/core/src/lib.rs | 2 + codex-rs/core/src/otel_init.rs | 61 + codex-rs/core/src/safety.rs | 35 +- .../core/tests/chat_completions_payload.rs | 17 +- codex-rs/core/tests/chat_completions_sse.rs | 112 +- codex-rs/core/tests/common/responses.rs | 46 +- codex-rs/core/tests/suite/abort_tasks.rs | 4 +- codex-rs/core/tests/suite/client.rs | 17 +- codex-rs/core/tests/suite/compact.rs | 8 +- .../core/tests/suite/compact_resume_fork.rs | 16 +- codex-rs/core/tests/suite/json_result.rs | 2 +- codex-rs/core/tests/suite/mod.rs | 1 + codex-rs/core/tests/suite/otel.rs | 1196 +++++++++++++++++ codex-rs/core/tests/suite/rmcp_client.rs | 10 +- .../core/tests/suite/user_notification.rs | 2 +- codex-rs/exec/Cargo.toml | 2 + codex-rs/exec/src/lib.rs | 55 +- codex-rs/exec/tests/suite/output_schema.rs | 2 +- .../exec/tests/suite/server_error_exit.rs | 2 +- codex-rs/otel/Cargo.toml | 39 + codex-rs/otel/src/config.rs | 33 + codex-rs/otel/src/lib.rs | 26 + codex-rs/otel/src/otel_event_manager.rs | 459 +++++++ codex-rs/otel/src/otel_provider.rs | 103 ++ codex-rs/protocol/src/mcp_protocol.rs | 2 +- codex-rs/protocol/src/protocol.rs | 2 +- codex-rs/tui/Cargo.toml | 1 + codex-rs/tui/src/lib.rs | 25 +- docs/config.md | 111 ++ 38 files changed, 3069 insertions(+), 142 deletions(-) create mode 100644 codex-rs/core/src/otel_init.rs create mode 100644 codex-rs/core/tests/suite/otel.rs create mode 100644 codex-rs/otel/Cargo.toml create mode 100644 codex-rs/otel/src/config.rs create mode 100644 codex-rs/otel/src/lib.rs create mode 100644 codex-rs/otel/src/otel_event_manager.rs create mode 100644 codex-rs/otel/src/otel_provider.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index fc0eb20a88..96bb30f73a 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -734,6 +734,7 @@ dependencies = [ "codex-apply-patch", "codex-file-search", "codex-mcp-client", + "codex-otel", "codex-protocol", "codex-rmcp-client", "core_test_support", @@ -771,6 +772,7 @@ dependencies = [ "toml", "toml_edit", "tracing", + "tracing-test", "tree-sitter", "tree-sitter-bash", "uuid", @@ -795,6 +797,7 @@ dependencies = [ "codex-protocol", "core_test_support", "libc", + "opentelemetry-appender-tracing", "owo-colors", "predicates", "pretty_assertions", @@ -948,6 +951,26 @@ dependencies = [ "wiremock", ] +[[package]] +name = "codex-otel" +version = "0.0.0" +dependencies = [ + "chrono", + "codex-protocol", + "eventsource-stream", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "reqwest", + "serde", + "serde_json", + "strum_macros 0.27.2", + "tokio", + "tonic", + "tracing", +] + [[package]] name = "codex-process-hardening" version = "0.0.0" @@ -1051,6 +1074,7 @@ dependencies = [ "lazy_static", "libc", "mcp-types", + "opentelemetry-appender-tracing", "path-clean", "pathdiff", "pretty_assertions", @@ -2301,6 +2325,7 @@ dependencies = [ "hyper", "hyper-util", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -2308,6 +2333,19 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -2342,7 +2380,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.0", "system-configuration", "tokio", "tower-service", @@ -3063,7 +3101,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -3349,6 +3387,104 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "opentelemetry-appender-tracing" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68f63eca5fad47e570e00e893094fc17be959c80c79a7d6ec1abdd5ae6ffc16" +dependencies = [ + "opentelemetry", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "opentelemetry-http" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" +dependencies = [ + "http", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "serde_json", + "thiserror 2.0.16", + "tokio", + "tonic", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" +dependencies = [ + "base64", + "hex", + "opentelemetry", + "opentelemetry_sdk", + "prost", + "serde", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d059a296a47436748557a353c5e6c5705b9470ef6c95cfc52c21a8814ddac2" + +[[package]] +name = "opentelemetry_sdk" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand", + "serde_json", + "thiserror 2.0.16", + "tokio", + "tokio-stream", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -3463,6 +3599,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3637,6 +3793,29 @@ dependencies = [ "windows", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "pulldown-cmark" version = "0.10.3" @@ -3682,9 +3861,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", "cfg_aliases 0.2.1", @@ -3693,7 +3872,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2", + "socket2 0.5.10", "thiserror 2.0.16", "tokio", "tracing", @@ -3702,9 +3881,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", "getrandom 0.3.3", @@ -3723,16 +3902,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ "cfg_aliases 0.2.1", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -3929,6 +4108,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls", + "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", @@ -4060,6 +4240,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.3.0", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -4265,6 +4457,19 @@ dependencies = [ "security-framework-sys", ] +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework-sys" version = "2.14.0" @@ -4530,6 +4735,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.0" @@ -5038,7 +5253,7 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2", + "socket2 0.6.0", "tokio-macros", "windows-sys 0.59.0", ] @@ -5163,6 +5378,35 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +[[package]] +name = "tonic" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +dependencies = [ + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2 0.5.10", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -5171,11 +5415,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap 2.10.0", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -5292,6 +5540,27 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tracing-test" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +dependencies = [ + "quote", + "syn 2.0.104", +] + [[package]] name = "tree-sitter" version = "0.25.9" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 862f06aa17..1c591897ee 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -21,6 +21,7 @@ members = [ "protocol-ts", "rmcp-client", "responses-api-proxy", + "otel", "tui", "utils/readiness", ] @@ -55,6 +56,7 @@ codex-protocol = { path = "protocol" } codex-protocol-ts = { path = "protocol-ts" } codex-rmcp-client = { path = "rmcp-client" } codex-tui = { path = "tui" } +codex-otel = { path = "otel" } codex-utils-readiness = { path = "utils/readiness" } core_test_support = { path = "core/tests/common" } mcp-types = { path = "mcp-types" } @@ -103,6 +105,11 @@ mime_guess = "2.0.5" multimap = "0.10.0" nucleo-matcher = "0.3.1" openssl-sys = "*" +opentelemetry = "0.30.0" +opentelemetry_sdk = "0.30.0" +opentelemetry-otlp = "0.30.0" +opentelemetry-appender-tracing = "0.30.0" +opentelemetry-semantic-conventions = "0.30.0" os_info = "3.12.0" owo-colors = "4.2.0" path-absolutize = "3.1.1" @@ -144,9 +151,11 @@ toml_edit = "0.23.4" tracing = "0.1.41" tracing-appender = "0.2.3" tracing-subscriber = "0.3.20" +tracing-test = "0.2.5" tree-sitter = "0.25.9" tree-sitter-bash = "0.25.0" ts-rs = "11" +tonic = "0.13.1" unicode-segmentation = "1.12.0" unicode-width = "0.2" url = "2" diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 9b1c4888a4..ff1103c3d2 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -24,6 +24,7 @@ codex-file-search = { workspace = true } codex-mcp-client = { workspace = true } codex-rmcp-client = { workspace = true } codex-protocol = { workspace = true } +codex-otel = { workspace = true, features = ["otel"] } dirs = { workspace = true } env-flags = { workspace = true } eventsource-stream = { workspace = true } @@ -91,6 +92,7 @@ tempfile = { workspace = true } tokio-test = { workspace = true } walkdir = { workspace = true } wiremock = { workspace = true } +tracing-test = { workspace = true, features = ["no-env-filter"] } [package.metadata.cargo-shear] ignored = ["openssl-sys"] diff --git a/codex-rs/core/src/apply_patch.rs b/codex-rs/core/src/apply_patch.rs index 1ebbe5d738..836b859633 100644 --- a/codex-rs/core/src/apply_patch.rs +++ b/codex-rs/core/src/apply_patch.rs @@ -45,12 +45,13 @@ pub(crate) async fn apply_patch( &turn_context.sandbox_policy, &turn_context.cwd, ) { - SafetyCheck::AutoApprove { .. } => { - InternalApplyPatchInvocation::DelegateToExec(ApplyPatchExec { - action, - user_explicitly_approved_this_action: false, - }) - } + SafetyCheck::AutoApprove { + user_explicitly_approved, + .. + } => InternalApplyPatchInvocation::DelegateToExec(ApplyPatchExec { + action, + user_explicitly_approved_this_action: user_explicitly_approved, + }), SafetyCheck::AskUser => { // Compute a readable summary of path changes to include in the // approval request so the user can make an informed decision. diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index eddc786484..f7194a0853 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -1,6 +1,19 @@ use std::time::Duration; +use crate::ModelProviderInfo; +use crate::client_common::Prompt; +use crate::client_common::ResponseEvent; +use crate::client_common::ResponseStream; +use crate::error::CodexErr; +use crate::error::Result; +use crate::model_family::ModelFamily; +use crate::openai_tools::create_tools_json_for_chat_completions_api; +use crate::util::backoff; use bytes::Bytes; +use codex_otel::otel_event_manager::OtelEventManager; +use codex_protocol::models::ContentItem; +use codex_protocol::models::ReasoningItemContent; +use codex_protocol::models::ResponseItem; use eventsource_stream::Eventsource; use futures::Stream; use futures::StreamExt; @@ -15,25 +28,13 @@ use tokio::time::timeout; use tracing::debug; use tracing::trace; -use crate::ModelProviderInfo; -use crate::client_common::Prompt; -use crate::client_common::ResponseEvent; -use crate::client_common::ResponseStream; -use crate::error::CodexErr; -use crate::error::Result; -use crate::model_family::ModelFamily; -use crate::openai_tools::create_tools_json_for_chat_completions_api; -use crate::util::backoff; -use codex_protocol::models::ContentItem; -use codex_protocol::models::ReasoningItemContent; -use codex_protocol::models::ResponseItem; - /// Implementation for the classic Chat Completions API. pub(crate) async fn stream_chat_completions( prompt: &Prompt, model_family: &ModelFamily, client: &reqwest::Client, provider: &ModelProviderInfo, + otel_event_manager: &OtelEventManager, ) -> Result { if prompt.output_schema.is_some() { return Err(CodexErr::UnsupportedOperation( @@ -294,10 +295,13 @@ pub(crate) async fn stream_chat_completions( let req_builder = provider.create_request_builder(client, &None).await?; - let res = req_builder - .header(reqwest::header::ACCEPT, "text/event-stream") - .json(&payload) - .send() + let res = otel_event_manager + .log_request(attempt, || { + req_builder + .header(reqwest::header::ACCEPT, "text/event-stream") + .json(&payload) + .send() + }) .await; match res { @@ -308,6 +312,7 @@ pub(crate) async fn stream_chat_completions( stream, tx_event, provider.stream_idle_timeout(), + otel_event_manager.clone(), )); return Ok(ResponseStream { rx_event }); } @@ -351,6 +356,7 @@ async fn process_chat_sse( stream: S, tx_event: mpsc::Sender>, idle_timeout: Duration, + otel_event_manager: OtelEventManager, ) where S: Stream> + Unpin, { @@ -374,7 +380,10 @@ async fn process_chat_sse( let mut reasoning_text = String::new(); loop { - let sse = match timeout(idle_timeout, stream.next()).await { + let sse = match otel_event_manager + .log_sse_event(|| timeout(idle_timeout, stream.next())) + .await + { Ok(Some(Ok(ev))) => ev, Ok(Some(Err(e))) => { let _ = tx_event diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index e8aca68fe8..685fbf45f2 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -47,6 +47,7 @@ use crate::protocol::RateLimitWindow; use crate::protocol::TokenUsage; use crate::token_data::PlanType; use crate::util::backoff; +use codex_otel::otel_event_manager::OtelEventManager; use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::models::ResponseItem; @@ -73,6 +74,7 @@ struct Error { pub struct ModelClient { config: Arc, auth_manager: Option>, + otel_event_manager: OtelEventManager, client: reqwest::Client, provider: ModelProviderInfo, conversation_id: ConversationId, @@ -84,6 +86,7 @@ impl ModelClient { pub fn new( config: Arc, auth_manager: Option>, + otel_event_manager: OtelEventManager, provider: ModelProviderInfo, effort: Option, summary: ReasoningSummaryConfig, @@ -94,6 +97,7 @@ impl ModelClient { Self { config, auth_manager, + otel_event_manager, client, provider, conversation_id, @@ -127,6 +131,7 @@ impl ModelClient { &self.config.model_family, &self.client, &self.provider, + &self.otel_event_manager, ) .await?; @@ -163,7 +168,12 @@ impl ModelClient { if let Some(path) = &*CODEX_RS_SSE_FIXTURE { // short circuit for tests warn!(path, "Streaming from fixture"); - return stream_from_fixture(path, self.provider.clone()).await; + return stream_from_fixture( + path, + self.provider.clone(), + self.otel_event_manager.clone(), + ) + .await; } let auth_manager = self.auth_manager.clone(); @@ -233,7 +243,7 @@ impl ModelClient { let max_attempts = self.provider.request_max_retries(); for attempt in 0..=max_attempts { match self - .attempt_stream_responses(&payload_json, &auth_manager) + .attempt_stream_responses(attempt, &payload_json, &auth_manager) .await { Ok(stream) => { @@ -258,6 +268,7 @@ impl ModelClient { /// Single attempt to start a streaming Responses API call. async fn attempt_stream_responses( &self, + attempt: u64, payload_json: &Value, auth_manager: &Option>, ) -> std::result::Result { @@ -291,7 +302,11 @@ impl ModelClient { req_builder = req_builder.header("chatgpt-account-id", account_id); } - let res = req_builder.send().await; + let res = self + .otel_event_manager + .log_request(attempt, || req_builder.send()) + .await; + if let Ok(resp) = &res { trace!( "Response status: {}, cf-ray: {}", @@ -322,6 +337,7 @@ impl ModelClient { stream, tx_event, self.provider.stream_idle_timeout(), + self.otel_event_manager.clone(), )); Ok(ResponseStream { rx_event }) @@ -399,6 +415,10 @@ impl ModelClient { self.provider.clone() } + pub fn get_otel_event_manager(&self) -> OtelEventManager { + self.otel_event_manager.clone() + } + /// Returns the currently configured model slug. pub fn get_model(&self) -> String { self.config.model.clone() @@ -605,6 +625,7 @@ async fn process_sse( stream: S, tx_event: mpsc::Sender>, idle_timeout: Duration, + otel_event_manager: OtelEventManager, ) where S: Stream> + Unpin, { @@ -616,7 +637,10 @@ async fn process_sse( let mut response_error: Option = None; loop { - let sse = match timeout(idle_timeout, stream.next()).await { + let sse = match otel_event_manager + .log_sse_event(|| timeout(idle_timeout, stream.next())) + .await + { Ok(Some(Ok(sse))) => sse, Ok(Some(Err(e))) => { debug!("SSE Error: {e:#}"); @@ -630,6 +654,21 @@ async fn process_sse( id: response_id, usage, }) => { + if let Some(token_usage) = &usage { + otel_event_manager.sse_event_completed( + token_usage.input_tokens, + token_usage.output_tokens, + token_usage + .input_tokens_details + .as_ref() + .map(|d| d.cached_tokens), + token_usage + .output_tokens_details + .as_ref() + .map(|d| d.reasoning_tokens), + token_usage.total_tokens, + ); + } let event = ResponseEvent::Completed { response_id, token_usage: usage.map(Into::into), @@ -637,12 +676,13 @@ async fn process_sse( let _ = tx_event.send(Ok(event)).await; } None => { - let _ = tx_event - .send(Err(response_error.unwrap_or(CodexErr::Stream( - "stream closed before response.completed".into(), - None, - )))) - .await; + let error = response_error.unwrap_or(CodexErr::Stream( + "stream closed before response.completed".into(), + None, + )); + otel_event_manager.see_event_completed_failed(&error); + + let _ = tx_event.send(Err(error)).await; } } return; @@ -746,7 +786,9 @@ async fn process_sse( response_error = Some(CodexErr::Stream(message, delay)); } Err(e) => { - debug!("failed to parse ErrorResponse: {e}"); + let error = format!("failed to parse ErrorResponse: {e}"); + debug!(error); + response_error = Some(CodexErr::Stream(error, None)) } } } @@ -760,7 +802,9 @@ async fn process_sse( response_completed = Some(r); } Err(e) => { - debug!("failed to parse ResponseCompleted: {e}"); + let error = format!("failed to parse ResponseCompleted: {e}"); + debug!(error); + response_error = Some(CodexErr::Stream(error, None)); continue; } }; @@ -807,6 +851,7 @@ async fn process_sse( async fn stream_from_fixture( path: impl AsRef, provider: ModelProviderInfo, + otel_event_manager: OtelEventManager, ) -> Result { let (tx_event, rx_event) = mpsc::channel::>(1600); let f = std::fs::File::open(path.as_ref())?; @@ -825,6 +870,7 @@ async fn stream_from_fixture( stream, tx_event, provider.stream_idle_timeout(), + otel_event_manager, )); Ok(ResponseStream { rx_event }) } @@ -880,6 +926,7 @@ mod tests { async fn collect_events( chunks: &[&[u8]], provider: ModelProviderInfo, + otel_event_manager: OtelEventManager, ) -> Vec> { let mut builder = IoBuilder::new(); for chunk in chunks { @@ -889,7 +936,12 @@ mod tests { let reader = builder.build(); let stream = ReaderStream::new(reader).map_err(CodexErr::Io); let (tx, mut rx) = mpsc::channel::>(16); - tokio::spawn(process_sse(stream, tx, provider.stream_idle_timeout())); + tokio::spawn(process_sse( + stream, + tx, + provider.stream_idle_timeout(), + otel_event_manager, + )); let mut events = Vec::new(); while let Some(ev) = rx.recv().await { @@ -903,6 +955,7 @@ mod tests { async fn run_sse( events: Vec, provider: ModelProviderInfo, + otel_event_manager: OtelEventManager, ) -> Vec { let mut body = String::new(); for e in events { @@ -919,7 +972,12 @@ mod tests { let (tx, mut rx) = mpsc::channel::>(8); let stream = ReaderStream::new(std::io::Cursor::new(body)).map_err(CodexErr::Io); - tokio::spawn(process_sse(stream, tx, provider.stream_idle_timeout())); + tokio::spawn(process_sse( + stream, + tx, + provider.stream_idle_timeout(), + otel_event_manager, + )); let mut out = Vec::new(); while let Some(ev) = rx.recv().await { @@ -928,6 +986,18 @@ mod tests { out } + fn otel_event_manager() -> OtelEventManager { + OtelEventManager::new( + ConversationId::new(), + "test", + "test", + None, + Some(AuthMode::ChatGPT), + false, + "test".to_string(), + ) + } + // ──────────────────────────── // Tests from `implement-test-for-responses-api-sse-parser` // ──────────────────────────── @@ -979,9 +1049,12 @@ mod tests { requires_openai_auth: false, }; + let otel_event_manager = otel_event_manager(); + let events = collect_events( &[sse1.as_bytes(), sse2.as_bytes(), sse3.as_bytes()], provider, + otel_event_manager, ) .await; @@ -1039,7 +1112,9 @@ mod tests { requires_openai_auth: false, }; - let events = collect_events(&[sse1.as_bytes()], provider).await; + let otel_event_manager = otel_event_manager(); + + let events = collect_events(&[sse1.as_bytes()], provider, otel_event_manager).await; assert_eq!(events.len(), 2); @@ -1073,7 +1148,9 @@ mod tests { requires_openai_auth: false, }; - let events = collect_events(&[sse1.as_bytes()], provider).await; + let otel_event_manager = otel_event_manager(); + + let events = collect_events(&[sse1.as_bytes()], provider, otel_event_manager).await; assert_eq!(events.len(), 1); @@ -1178,7 +1255,9 @@ mod tests { requires_openai_auth: false, }; - let out = run_sse(evs, provider).await; + let otel_event_manager = otel_event_manager(); + + let out = run_sse(evs, provider, otel_event_manager).await; assert_eq!(out.len(), case.expected_len, "case {}", case.name); assert!( (case.expect_first)(&out[0]), diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 8b792887c0..7a2b2c8da0 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::collections::HashMap; +use std::fmt::Debug; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; @@ -11,6 +12,7 @@ use crate::client_common::REVIEW_PROMPT; use crate::event_mapping::map_response_item_to_event_messages; use crate::function_tool::FunctionCallError; use crate::review_format::format_review_findings_block; +use crate::terminal; use crate::user_notification::UserNotifier; use async_channel::Receiver; use async_channel::Sender; @@ -125,6 +127,8 @@ use crate::unified_exec::UnifiedExecSessionManager; use crate::user_instructions::UserInstructions; use crate::user_notification::UserNotification; use crate::util::backoff; +use codex_otel::otel_event_manager::OtelEventManager; +use codex_otel::otel_event_manager::ToolDecisionSource; use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::custom_prompts::CustomPrompt; @@ -422,11 +426,35 @@ impl Session { } } + let otel_event_manager = OtelEventManager::new( + conversation_id, + config.model.as_str(), + config.model_family.slug.as_str(), + auth_manager.auth().and_then(|a| a.get_account_id()), + auth_manager.auth().map(|a| a.mode), + config.otel.log_user_prompt, + terminal::user_agent(), + ); + + otel_event_manager.conversation_starts( + config.model_provider.name.as_str(), + config.model_reasoning_effort, + config.model_reasoning_summary, + config.model_context_window, + config.model_max_output_tokens, + config.model_auto_compact_token_limit, + config.approval_policy, + config.sandbox_policy.clone(), + config.mcp_servers.keys().map(String::as_str).collect(), + config.active_profile.clone(), + ); + // Now that the conversation id is final (may have been updated by resume), // construct the model client. let client = ModelClient::new( config.clone(), Some(auth_manager.clone()), + otel_event_manager, provider.clone(), model_reasoning_effort, model_reasoning_summary, @@ -1122,9 +1150,15 @@ async fn submission_loop( updated_config.model_context_window = Some(model_info.context_window); } + let otel_event_manager = prev.client.get_otel_event_manager().with_model( + updated_config.model.as_str(), + updated_config.model_family.slug.as_str(), + ); + let client = ModelClient::new( Arc::new(updated_config), auth_manager, + otel_event_manager, provider, effective_effort, effective_summary, @@ -1176,6 +1210,10 @@ async fn submission_loop( } } Op::UserInput { items } => { + turn_context + .client + .get_otel_event_manager() + .user_prompt(&items); // attempt to inject input into current task if let Err(items) = sess.inject_input(items).await { // no current task, spawn a new one @@ -1193,6 +1231,10 @@ async fn submission_loop( summary, final_output_json_schema, } => { + turn_context + .client + .get_otel_event_manager() + .user_prompt(&items); // attempt to inject input into current task if let Err(items) = sess.inject_input(items).await { // Derive a fresh TurnContext for this turn using the provided overrides. @@ -1211,11 +1253,18 @@ async fn submission_loop( per_turn_config.model_context_window = Some(model_info.context_window); } + let otel_event_manager = + turn_context.client.get_otel_event_manager().with_model( + per_turn_config.model.as_str(), + per_turn_config.model_family.slug.as_str(), + ); + // Build a new client with per‑turn reasoning settings. // Reuse the same provider and session id; auth defaults to env/API key. let client = ModelClient::new( Arc::new(per_turn_config), auth_manager, + otel_event_manager, provider, effort, summary, @@ -1472,10 +1521,19 @@ async fn spawn_review_thread( per_turn_config.model_context_window = Some(model_info.context_window); } + let otel_event_manager = parent_turn_context + .client + .get_otel_event_manager() + .with_model( + per_turn_config.model.as_str(), + per_turn_config.model_family.slug.as_str(), + ); + let per_turn_config = Arc::new(per_turn_config); let client = ModelClient::new( per_turn_config.clone(), auth_manager, + otel_event_manager, provider, per_turn_config.model_reasoning_effort, per_turn_config.model_reasoning_summary, @@ -2140,16 +2198,21 @@ async fn handle_response_item( .await; Some(resp) } else { - let result = handle_function_call( - sess, - turn_context, - turn_diff_tracker, - sub_id.to_string(), - name, - arguments, - call_id.clone(), - ) - .await; + let result = turn_context + .client + .get_otel_event_manager() + .log_tool_result(name.as_str(), call_id.as_str(), arguments.as_str(), || { + handle_function_call( + sess, + turn_context, + turn_diff_tracker, + sub_id.to_string(), + name.to_owned(), + arguments.to_owned(), + call_id.clone(), + ) + }) + .await; let output = match result { Ok(content) => FunctionCallOutputPayload { @@ -2170,6 +2233,7 @@ async fn handle_response_item( status: _, action, } => { + let name = "local_shell"; let LocalShellAction::Exec(action) = action; tracing::info!("LocalShellCall: {action:?}"); let params = ShellToolCallParams { @@ -2183,11 +2247,18 @@ async fn handle_response_item( (Some(call_id), _) => call_id, (None, Some(id)) => id, (None, None) => { - error!("LocalShellCall without call_id or id"); + let error_message = "LocalShellCall without call_id or id"; + + turn_context + .client + .get_otel_event_manager() + .log_tool_failed(name, error_message); + + error!(error_message); return Ok(Some(ResponseInputItem::FunctionCallOutput { call_id: "".to_string(), output: FunctionCallOutputPayload { - content: "LocalShellCall without call_id or id".to_string(), + content: error_message.to_string(), success: None, }, })); @@ -2196,15 +2267,26 @@ async fn handle_response_item( let exec_params = to_exec_params(params, turn_context); { - let result = handle_container_exec_with_params( - exec_params, - sess, - turn_context, - turn_diff_tracker, - sub_id.to_string(), - effective_call_id.clone(), - ) - .await; + let result = turn_context + .client + .get_otel_event_manager() + .log_tool_result( + name, + effective_call_id.as_str(), + exec_params.command.join(" ").as_str(), + || { + handle_container_exec_with_params( + name, + exec_params, + sess, + turn_context, + turn_diff_tracker, + sub_id.to_string(), + effective_call_id.clone(), + ) + }, + ) + .await; let output = match result { Ok(content) => FunctionCallOutputPayload { @@ -2229,16 +2311,21 @@ async fn handle_response_item( input, status: _, } => { - let result = handle_custom_tool_call( - sess, - turn_context, - turn_diff_tracker, - sub_id.to_string(), - name, - input, - call_id.clone(), - ) - .await; + let result = turn_context + .client + .get_otel_event_manager() + .log_tool_result(name.as_str(), call_id.as_str(), input.as_str(), || { + handle_custom_tool_call( + sess, + turn_context, + turn_diff_tracker, + sub_id.to_string(), + name.to_owned(), + input.to_owned(), + call_id.clone(), + ) + }) + .await; let output = match result { Ok(content) => content, @@ -2344,6 +2431,7 @@ async fn handle_function_call( "container.exec" | "shell" => { let params = parse_container_exec_arguments(arguments, turn_context, &call_id)?; handle_container_exec_with_params( + name.as_str(), params, sess, turn_context, @@ -2407,6 +2495,7 @@ async fn handle_function_call( justification: None, }; handle_container_exec_with_params( + name.as_str(), exec_params, sess, turn_context, @@ -2479,6 +2568,7 @@ async fn handle_custom_tool_call( }; handle_container_exec_with_params( + name.as_str(), exec_params, sess, turn_context, @@ -2548,6 +2638,7 @@ fn maybe_translate_shell_command( } async fn handle_container_exec_with_params( + tool_name: &str, params: ExecParams, sess: &Session, turn_context: &TurnContext, @@ -2555,6 +2646,8 @@ async fn handle_container_exec_with_params( sub_id: String, call_id: String, ) -> Result { + let otel_event_manager = turn_context.client.get_otel_event_manager(); + if params.with_escalated_permissions.unwrap_or(false) && !matches!(turn_context.approval_policy, AskForApproval::OnRequest) { @@ -2618,6 +2711,7 @@ async fn handle_container_exec_with_params( let safety = if *user_explicitly_approved_this_action { SafetyCheck::AutoApprove { sandbox_type: SandboxType::None, + user_explicitly_approved: true, } } else { assess_safety_for_untrusted_command( @@ -2649,7 +2743,23 @@ async fn handle_container_exec_with_params( }; let sandbox_type = match safety { - SafetyCheck::AutoApprove { sandbox_type } => sandbox_type, + SafetyCheck::AutoApprove { + sandbox_type, + user_explicitly_approved, + } => { + otel_event_manager.tool_decision( + tool_name, + call_id.as_str(), + ReviewDecision::Approved, + if user_explicitly_approved { + ToolDecisionSource::User + } else { + ToolDecisionSource::Config + }, + ); + + sandbox_type + } SafetyCheck::AskUser => { let decision = sess .request_command_approval( @@ -2661,15 +2771,45 @@ async fn handle_container_exec_with_params( ) .await; match decision { - ReviewDecision::Approved => (), + ReviewDecision::Approved => { + otel_event_manager.tool_decision( + tool_name, + call_id.as_str(), + ReviewDecision::Approved, + ToolDecisionSource::User, + ); + } ReviewDecision::ApprovedForSession => { + otel_event_manager.tool_decision( + tool_name, + call_id.as_str(), + ReviewDecision::ApprovedForSession, + ToolDecisionSource::User, + ); sess.add_approved_command(params.command.clone()).await; } - ReviewDecision::Denied | ReviewDecision::Abort => { + ReviewDecision::Denied => { + otel_event_manager.tool_decision( + tool_name, + call_id.as_str(), + ReviewDecision::Denied, + ToolDecisionSource::User, + ); return Err(FunctionCallError::RespondToModel( "exec command rejected by user".to_string(), )); } + ReviewDecision::Abort => { + otel_event_manager.tool_decision( + tool_name, + call_id.as_str(), + ReviewDecision::Abort, + ToolDecisionSource::User, + ); + return Err(FunctionCallError::RespondToModel( + "exec command aborted by user".to_string(), + )); + } } // No sandboxing is applied because the user has given // explicit approval. Often, we end up in this case because @@ -2678,6 +2818,12 @@ async fn handle_container_exec_with_params( SandboxType::None } SafetyCheck::Reject { reason } => { + otel_event_manager.tool_decision( + tool_name, + call_id.as_str(), + ReviewDecision::Denied, + ToolDecisionSource::Config, + ); return Err(FunctionCallError::RespondToModel(format!( "exec command rejected: {reason:?}" ))); @@ -2736,6 +2882,7 @@ async fn handle_container_exec_with_params( } Err(CodexErr::Sandbox(error)) => { handle_sandbox_error( + tool_name, turn_diff_tracker, params, exec_command_context, @@ -2743,6 +2890,7 @@ async fn handle_container_exec_with_params( sandbox_type, sess, turn_context, + &otel_event_manager, ) .await } @@ -2752,7 +2900,9 @@ async fn handle_container_exec_with_params( } } +#[allow(clippy::too_many_arguments)] async fn handle_sandbox_error( + tool_name: &str, turn_diff_tracker: &mut TurnDiffTracker, params: ExecParams, exec_command_context: ExecCommandContext, @@ -2760,6 +2910,7 @@ async fn handle_sandbox_error( sandbox_type: SandboxType, sess: &Session, turn_context: &TurnContext, + otel_event_manager: &OtelEventManager, ) -> Result { let call_id = exec_command_context.call_id.clone(); let sub_id = exec_command_context.sub_id.clone(); @@ -2814,6 +2965,13 @@ async fn handle_sandbox_error( sess.notify_background_event(&sub_id, "retrying command without sandbox") .await; + otel_event_manager.tool_decision( + tool_name, + call_id.as_str(), + decision, + ToolDecisionSource::User, + ); + // This is an escalated retry; the policy will not be // examined and the sandbox has been set to `None`. let retry_output_result = sess @@ -2854,7 +3012,14 @@ async fn handle_sandbox_error( ))), } } - ReviewDecision::Denied | ReviewDecision::Abort => { + decision @ (ReviewDecision::Denied | ReviewDecision::Abort) => { + otel_event_manager.tool_decision( + tool_name, + call_id.as_str(), + decision, + ToolDecisionSource::User, + ); + // Fall through to original failure handling. Err(FunctionCallError::RespondToModel( "exec command rejected by user".to_string(), @@ -3129,13 +3294,17 @@ mod tests { use super::*; use crate::config::ConfigOverrides; use crate::config::ConfigToml; + use crate::protocol::CompactedItem; use crate::protocol::InitialHistory; use crate::protocol::ResumedHistory; use crate::state::TaskKind; use crate::tasks::SessionTask; use crate::tasks::SessionTaskContext; + use codex_protocol::mcp_protocol::AuthMode; use codex_protocol::models::ContentItem; + use codex_protocol::models::ResponseItem; + use mcp_types::ContentBlock; use mcp_types::TextContent; use pretty_assertions::assert_eq; @@ -3370,6 +3539,18 @@ mod tests { }) } + fn otel_event_manager(conversation_id: ConversationId, config: &Config) -> OtelEventManager { + OtelEventManager::new( + conversation_id, + config.model.as_str(), + config.model_family.slug.as_str(), + None, + Some(AuthMode::ChatGPT), + false, + "test".to_string(), + ) + } + pub(crate) fn make_session_and_context() -> (Session, TurnContext) { let (tx_event, _rx_event) = async_channel::unbounded(); let codex_home = tempfile::tempdir().expect("create temp dir"); @@ -3381,9 +3562,11 @@ mod tests { .expect("load default test config"); let config = Arc::new(config); let conversation_id = ConversationId::default(); + let otel_event_manager = otel_event_manager(conversation_id, config.as_ref()); let client = ModelClient::new( config.clone(), None, + otel_event_manager, config.model_provider.clone(), config.model_reasoning_effort, config.model_reasoning_summary, @@ -3448,9 +3631,11 @@ mod tests { .expect("load default test config"); let config = Arc::new(config); let conversation_id = ConversationId::default(); + let otel_event_manager = otel_event_manager(conversation_id, config.as_ref()); let client = ModelClient::new( config.clone(), None, + otel_event_manager, config.model_provider.clone(), config.model_reasoning_effort, config.model_reasoning_summary, @@ -3741,10 +3926,12 @@ mod tests { let mut turn_diff_tracker = TurnDiffTracker::new(); + let tool_name = "shell"; let sub_id = "test-sub".to_string(); let call_id = "test-call".to_string(); let resp = handle_container_exec_with_params( + tool_name, params, &session, &turn_context, @@ -3770,6 +3957,7 @@ mod tests { turn_context.sandbox_policy = SandboxPolicy::DangerFullAccess; let resp2 = handle_container_exec_with_params( + tool_name, params2, &session, &turn_context, diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 292b9f7b51..7d61620488 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -1,8 +1,12 @@ use crate::config_profile::ConfigProfile; +use crate::config_types::DEFAULT_OTEL_ENVIRONMENT; use crate::config_types::History; use crate::config_types::McpServerConfig; use crate::config_types::McpServerTransportConfig; use crate::config_types::Notifications; +use crate::config_types::OtelConfig; +use crate::config_types::OtelConfigToml; +use crate::config_types::OtelExporterKind; use crate::config_types::ReasoningSummaryFormat; use crate::config_types::SandboxWorkspaceWrite; use crate::config_types::ShellEnvironmentPolicy; @@ -199,6 +203,9 @@ pub struct Config { /// All characters are inserted as they are received, and no buffering /// or placeholder replacement will occur for fast keypress bursts. pub disable_paste_burst: bool, + + /// OTEL configuration (exporter type, endpoint, headers, etc.). + pub otel: crate::config_types::OtelConfig, } impl Config { @@ -719,6 +726,9 @@ pub struct ConfigToml { /// All characters are inserted as they are received, and no buffering /// or placeholder replacement will occur for fast keypress bursts. pub disable_paste_burst: Option, + + /// OTEL configuration. + pub otel: Option, } impl From for UserSavedConfig { @@ -1068,6 +1078,19 @@ impl Config { .as_ref() .map(|t| t.notifications.clone()) .unwrap_or_default(), + otel: { + let t: OtelConfigToml = cfg.otel.unwrap_or_default(); + let log_user_prompt = t.log_user_prompt.unwrap_or(false); + let environment = t + .environment + .unwrap_or(DEFAULT_OTEL_ENVIRONMENT.to_string()); + let exporter = t.exporter.unwrap_or(OtelExporterKind::None); + OtelConfig { + log_user_prompt, + environment, + exporter, + } + }, }; Ok(config) } @@ -1809,6 +1832,7 @@ model_verbosity = "high" active_profile: Some("o3".to_string()), disable_paste_burst: false, tui_notifications: Default::default(), + otel: OtelConfig::default(), }, o3_profile_config ); @@ -1868,6 +1892,7 @@ model_verbosity = "high" active_profile: Some("gpt3".to_string()), disable_paste_burst: false, tui_notifications: Default::default(), + otel: OtelConfig::default(), }; assert_eq!(expected_gpt3_profile_config, gpt3_profile_config); @@ -1942,6 +1967,7 @@ model_verbosity = "high" active_profile: Some("zdr".to_string()), disable_paste_burst: false, tui_notifications: Default::default(), + otel: OtelConfig::default(), }; assert_eq!(expected_zdr_profile_config, zdr_profile_config); @@ -2002,6 +2028,7 @@ model_verbosity = "high" active_profile: Some("gpt5".to_string()), disable_paste_burst: false, tui_notifications: Default::default(), + otel: OtelConfig::default(), }; assert_eq!(expected_gpt5_profile_config, gpt5_profile_config); diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index 283ae3a480..4728e76ef4 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -13,6 +13,8 @@ use serde::Deserialize; use serde::Serialize; use serde::de::Error as SerdeError; +pub const DEFAULT_OTEL_ENVIRONMENT: &str = "dev"; + #[derive(Serialize, Debug, Clone, PartialEq)] pub struct McpServerConfig { #[serde(flatten)] @@ -219,6 +221,64 @@ pub enum HistoryPersistence { None, } +// ===== OTEL configuration ===== + +#[derive(Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum OtelHttpProtocol { + /// Binary payload + Binary, + /// JSON payload + Json, +} + +/// Which OTEL exporter to use. +#[derive(Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum OtelExporterKind { + None, + OtlpHttp { + endpoint: String, + headers: HashMap, + protocol: OtelHttpProtocol, + }, + OtlpGrpc { + endpoint: String, + headers: HashMap, + }, +} + +/// OTEL settings loaded from config.toml. Fields are optional so we can apply defaults. +#[derive(Deserialize, Debug, Clone, PartialEq, Default)] +pub struct OtelConfigToml { + /// Log user prompt in traces + pub log_user_prompt: Option, + + /// Mark traces with environment (dev, staging, prod, test). Defaults to dev. + pub environment: Option, + + /// Exporter to use. Defaults to `otlp-file`. + pub exporter: Option, +} + +/// Effective OTEL settings after defaults are applied. +#[derive(Debug, Clone, PartialEq)] +pub struct OtelConfig { + pub log_user_prompt: bool, + pub environment: String, + pub exporter: OtelExporterKind, +} + +impl Default for OtelConfig { + fn default() -> Self { + OtelConfig { + log_user_prompt: false, + environment: DEFAULT_OTEL_ENVIRONMENT.to_owned(), + exporter: OtelExporterKind::None, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(untagged)] pub enum Notifications { diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index f3f6106692..7d087a0da2 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -103,3 +103,5 @@ pub use codex_protocol::models::LocalShellExecAction; pub use codex_protocol::models::LocalShellStatus; pub use codex_protocol::models::ReasoningItemContent; pub use codex_protocol::models::ResponseItem; + +pub mod otel_init; diff --git a/codex-rs/core/src/otel_init.rs b/codex-rs/core/src/otel_init.rs new file mode 100644 index 0000000000..0d6002c225 --- /dev/null +++ b/codex-rs/core/src/otel_init.rs @@ -0,0 +1,61 @@ +use crate::config::Config; +use crate::config_types::OtelExporterKind as Kind; +use crate::config_types::OtelHttpProtocol as Protocol; +use crate::default_client::ORIGINATOR; +use codex_otel::config::OtelExporter; +use codex_otel::config::OtelHttpProtocol; +use codex_otel::config::OtelSettings; +use codex_otel::otel_provider::OtelProvider; +use std::error::Error; + +/// Build an OpenTelemetry provider from the app Config. +/// +/// Returns `None` when OTEL export is disabled. +pub fn build_provider( + config: &Config, + service_version: &str, +) -> Result, Box> { + let exporter = match &config.otel.exporter { + Kind::None => OtelExporter::None, + Kind::OtlpHttp { + endpoint, + headers, + protocol, + } => { + let protocol = match protocol { + Protocol::Json => OtelHttpProtocol::Json, + Protocol::Binary => OtelHttpProtocol::Binary, + }; + + OtelExporter::OtlpHttp { + endpoint: endpoint.clone(), + headers: headers + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + protocol, + } + } + Kind::OtlpGrpc { endpoint, headers } => OtelExporter::OtlpGrpc { + endpoint: endpoint.clone(), + headers: headers + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + }, + }; + + OtelProvider::from(&OtelSettings { + service_name: ORIGINATOR.value.to_owned(), + service_version: service_version.to_string(), + codex_home: config.codex_home.clone(), + environment: config.otel.environment.to_string(), + exporter, + }) +} + +/// Filter predicate for exporting only Codex-owned events via OTEL. +/// Keeps events that originated from codex_otel module +pub fn codex_export_filter(meta: &tracing::Metadata<'_>) -> bool { + meta.target().starts_with("codex_otel") +} diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 0c8dfd9a98..b976ae4a4c 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -15,9 +15,14 @@ use crate::protocol::SandboxPolicy; #[derive(Debug, PartialEq)] pub enum SafetyCheck { - AutoApprove { sandbox_type: SandboxType }, + AutoApprove { + sandbox_type: SandboxType, + user_explicitly_approved: bool, + }, AskUser, - Reject { reason: String }, + Reject { + reason: String, + }, } pub fn assess_patch_safety( @@ -54,12 +59,16 @@ pub fn assess_patch_safety( // fall back to asking the user because the patch may touch arbitrary // paths outside the project. match get_platform_sandbox() { - Some(sandbox_type) => SafetyCheck::AutoApprove { sandbox_type }, + Some(sandbox_type) => SafetyCheck::AutoApprove { + sandbox_type, + user_explicitly_approved: false, + }, None if sandbox_policy == &SandboxPolicy::DangerFullAccess => { // If the user has explicitly requested DangerFullAccess, then // we can auto-approve even without a sandbox. SafetyCheck::AutoApprove { sandbox_type: SandboxType::None, + user_explicitly_approved: false, } } None => SafetyCheck::AskUser, @@ -118,6 +127,7 @@ pub fn assess_command_safety( if is_known_safe_command(command) || approved.contains(command) { return SafetyCheck::AutoApprove { sandbox_type: SandboxType::None, + user_explicitly_approved: false, }; } @@ -143,13 +153,17 @@ pub(crate) fn assess_safety_for_untrusted_command( | (Never, DangerFullAccess) | (OnRequest, DangerFullAccess) => SafetyCheck::AutoApprove { sandbox_type: SandboxType::None, + user_explicitly_approved: false, }, (OnRequest, ReadOnly) | (OnRequest, WorkspaceWrite { .. }) => { if with_escalated_permissions { SafetyCheck::AskUser } else { match get_platform_sandbox() { - Some(sandbox_type) => SafetyCheck::AutoApprove { sandbox_type }, + Some(sandbox_type) => SafetyCheck::AutoApprove { + sandbox_type, + user_explicitly_approved: false, + }, // Fall back to asking since the command is untrusted and // we do not have a sandbox available None => SafetyCheck::AskUser, @@ -161,7 +175,10 @@ pub(crate) fn assess_safety_for_untrusted_command( | (OnFailure, ReadOnly) | (OnFailure, WorkspaceWrite { .. }) => { match get_platform_sandbox() { - Some(sandbox_type) => SafetyCheck::AutoApprove { sandbox_type }, + Some(sandbox_type) => SafetyCheck::AutoApprove { + sandbox_type, + user_explicitly_approved: false, + }, None => { if matches!(approval_policy, OnFailure) { // Since the command is not trusted, even though the @@ -362,7 +379,8 @@ mod tests { assert_eq!( safety_check, SafetyCheck::AutoApprove { - sandbox_type: SandboxType::None + sandbox_type: SandboxType::None, + user_explicitly_approved: false, } ); } @@ -409,7 +427,10 @@ mod tests { ); let expected = match get_platform_sandbox() { - Some(sandbox_type) => SafetyCheck::AutoApprove { sandbox_type }, + Some(sandbox_type) => SafetyCheck::AutoApprove { + sandbox_type, + user_explicitly_approved: false, + }, None => SafetyCheck::AskUser, }; assert_eq!(safety_check, expected); diff --git a/codex-rs/core/tests/chat_completions_payload.rs b/codex-rs/core/tests/chat_completions_payload.rs index ba3fe9de79..4551c5ca59 100644 --- a/codex-rs/core/tests/chat_completions_payload.rs +++ b/codex-rs/core/tests/chat_completions_payload.rs @@ -11,6 +11,8 @@ use codex_core::ReasoningItemContent; use codex_core::ResponseItem; use codex_core::WireApi; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; +use codex_otel::otel_event_manager::OtelEventManager; +use codex_protocol::mcp_protocol::AuthMode; use codex_protocol::mcp_protocol::ConversationId; use core_test_support::load_default_config_for_test; use futures::StreamExt; @@ -70,13 +72,26 @@ async fn run_request(input: Vec) -> Value { let summary = config.model_reasoning_summary; let config = Arc::new(config); + let conversation_id = ConversationId::new(); + + let otel_event_manager = OtelEventManager::new( + conversation_id, + config.model.as_str(), + config.model_family.slug.as_str(), + None, + Some(AuthMode::ChatGPT), + false, + "test".to_string(), + ); + let client = ModelClient::new( Arc::clone(&config), None, + otel_event_manager, provider, effort, summary, - ConversationId::new(), + conversation_id, ); let mut prompt = Prompt::default(); diff --git a/codex-rs/core/tests/chat_completions_sse.rs b/codex-rs/core/tests/chat_completions_sse.rs index 6155d15e62..9338a1af15 100644 --- a/codex-rs/core/tests/chat_completions_sse.rs +++ b/codex-rs/core/tests/chat_completions_sse.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use tracing_test::traced_test; use codex_core::ContentItem; use codex_core::ModelClient; @@ -8,6 +9,8 @@ use codex_core::ResponseEvent; use codex_core::ResponseItem; use codex_core::WireApi; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; +use codex_otel::otel_event_manager::OtelEventManager; +use codex_protocol::mcp_protocol::AuthMode; use codex_protocol::mcp_protocol::ConversationId; use core_test_support::load_default_config_for_test; use futures::StreamExt; @@ -23,11 +26,15 @@ fn network_disabled() -> bool { } async fn run_stream(sse_body: &str) -> Vec { + run_stream_with_bytes(sse_body.as_bytes()).await +} + +async fn run_stream_with_bytes(sse_body: &[u8]) -> Vec { let server = MockServer::start().await; let template = ResponseTemplate::new(200) .insert_header("content-type", "text/event-stream") - .set_body_raw(sse_body.to_string(), "text/event-stream"); + .set_body_bytes(sse_body.to_vec()); Mock::given(method("POST")) .and(path("/v1/chat/completions")) @@ -63,13 +70,26 @@ async fn run_stream(sse_body: &str) -> Vec { let summary = config.model_reasoning_summary; let config = Arc::new(config); + let conversation_id = ConversationId::new(); + + let otel_event_manager = OtelEventManager::new( + conversation_id, + config.model.as_str(), + config.model_family.slug.as_str(), + None, + Some(AuthMode::ChatGPT), + false, + "test".to_string(), + ); + let client = ModelClient::new( Arc::clone(&config), None, + otel_event_manager, provider, effort, summary, - ConversationId::new(), + conversation_id, ); let mut prompt = Prompt::default(); @@ -89,7 +109,8 @@ async fn run_stream(sse_body: &str) -> Vec { while let Some(event) = stream.next().await { match event { Ok(ev) => events.push(ev), - Err(e) => panic!("stream event error: {e}"), + // We still collect the error to exercise telemetry and complete the task. + Err(_e) => break, } } events @@ -318,3 +339,88 @@ async fn streams_reasoning_before_tool_call() { assert!(matches!(events[3], ResponseEvent::Completed { .. })); } + +#[tokio::test] +#[traced_test] +async fn chat_sse_emits_failed_on_parse_error() { + if network_disabled() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + let sse_body = concat!("data: not-json\n\n", "data: [DONE]\n\n"); + + let _ = run_stream(sse_body).await; + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| { + line.contains("codex.api_request") && line.contains("http.response.status_code=200") + }) + .map(|_| Ok(())) + .unwrap_or(Err("cannot find codex.api_request event".to_string())) + }); + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| { + line.contains("codex.sse_event") + && line.contains("error.message") + && line.contains("expected ident at line 1 column 2") + }) + .map(|_| Ok(())) + .unwrap_or(Err("cannot find SSE event".to_string())) + }); +} + +#[tokio::test] +#[traced_test] +async fn chat_sse_done_chunk_emits_event() { + if network_disabled() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + let sse_body = "data: [DONE]\n\n"; + + let _ = run_stream(sse_body).await; + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| line.contains("codex.sse_event") && line.contains("event.kind=message")) + .map(|_| Ok(())) + .unwrap_or(Err("cannot find SSE event".to_string())) + }); +} + +#[tokio::test] +#[traced_test] +async fn chat_sse_emits_error_on_invalid_utf8() { + if network_disabled() { + println!( + "Skipping test because it cannot execute when network is disabled in a Codex sandbox." + ); + return; + } + + let _ = run_stream_with_bytes(b"data: \x80\x80\n\n").await; + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| { + line.contains("codex.sse_event") + && line.contains("error.message") + && line.contains("UTF8 error: invalid utf-8 sequence of 1 bytes from index 0") + }) + .map(|_| Ok(())) + .unwrap_or(Err("cannot find SSE event".to_string())) + }); +} diff --git a/codex-rs/core/tests/common/responses.rs b/codex-rs/core/tests/common/responses.rs index 6ba10a7042..b13e7599ef 100644 --- a/codex-rs/core/tests/common/responses.rs +++ b/codex-rs/core/tests/common/responses.rs @@ -75,6 +75,33 @@ pub fn ev_function_call(call_id: &str, name: &str, arguments: &str) -> Value { }) } +pub fn ev_custom_tool_call(call_id: &str, name: &str, input: &str) -> Value { + serde_json::json!({ + "type": "response.output_item.done", + "item": { + "type": "custom_tool_call", + "call_id": call_id, + "name": name, + "input": input + } + }) +} + +pub fn ev_local_shell_call(call_id: &str, status: &str, command: Vec<&str>) -> Value { + serde_json::json!({ + "type": "response.output_item.done", + "item": { + "type": "local_shell_call", + "call_id": call_id, + "status": status, + "action": { + "type": "exec", + "command": command, + } + } + }) +} + /// Convenience: SSE event for an `apply_patch` custom tool call with raw patch /// text. This mirrors the payload produced by the Responses API when the model /// invokes `apply_patch` directly (before we convert it to a function call). @@ -114,7 +141,7 @@ pub fn sse_response(body: String) -> ResponseTemplate { .set_body_raw(body, "text/event-stream") } -pub async fn mount_sse_once(server: &MockServer, matcher: M, body: String) +pub async fn mount_sse_once_match(server: &MockServer, matcher: M, body: String) where M: wiremock::Match + Send + Sync + 'static, { @@ -127,6 +154,23 @@ where .await; } +pub async fn mount_sse_once(server: &MockServer, body: String) { + Mock::given(method("POST")) + .and(path("/v1/responses")) + .respond_with(sse_response(body)) + .expect(1) + .mount(server) + .await; +} + +pub async fn mount_sse(server: &MockServer, body: String) { + Mock::given(method("POST")) + .and(path("/v1/responses")) + .respond_with(sse_response(body)) + .mount(server) + .await; +} + pub async fn start_mock_server() -> MockServer { MockServer::builder() .body_print_limit(BodyPrintLimit::Limited(80_000)) diff --git a/codex-rs/core/tests/suite/abort_tasks.rs b/codex-rs/core/tests/suite/abort_tasks.rs index 2fa3d4df6b..368f7f74a8 100644 --- a/codex-rs/core/tests/suite/abort_tasks.rs +++ b/codex-rs/core/tests/suite/abort_tasks.rs @@ -4,7 +4,7 @@ use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use core_test_support::responses::ev_function_call; -use core_test_support::responses::mount_sse_once; +use core_test_support::responses::mount_sse_once_match; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::test_codex::test_codex; @@ -30,7 +30,7 @@ async fn interrupt_long_running_tool_emits_turn_aborted() { let body = sse(vec![ev_function_call("call_sleep", "shell", &args)]); let server = start_mock_server().await; - mount_sse_once(&server, body_string_contains("start sleep"), body).await; + mount_sse_once_match(&server, body_string_contains("start sleep"), body).await; let codex = test_codex().build(&server).await.unwrap().codex; diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index cf15e02d29..7b8eb42916 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -16,6 +16,8 @@ use codex_core::built_in_model_providers; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; +use codex_otel::otel_event_manager::OtelEventManager; +use codex_protocol::mcp_protocol::AuthMode; use codex_protocol::mcp_protocol::ConversationId; use codex_protocol::models::ReasoningItemReasoningSummary; use codex_protocol::models::WebSearchAction; @@ -664,13 +666,26 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { let summary = config.model_reasoning_summary; let config = Arc::new(config); + let conversation_id = ConversationId::new(); + + let otel_event_manager = OtelEventManager::new( + conversation_id, + config.model.as_str(), + config.model_family.slug.as_str(), + None, + Some(AuthMode::ChatGPT), + false, + "test".to_string(), + ); + let client = ModelClient::new( Arc::clone(&config), None, + otel_event_manager, provider, effort, summary, - ConversationId::new(), + conversation_id, ); let mut prompt = Prompt::default(); diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index ede25588c0..a8515296e8 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -25,7 +25,7 @@ use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed_with_tokens; use core_test_support::responses::ev_function_call; -use core_test_support::responses::mount_sse_once; +use core_test_support::responses::mount_sse_once_match; use core_test_support::responses::sse; use core_test_support::responses::sse_response; use core_test_support::responses::start_mock_server; @@ -79,19 +79,19 @@ async fn summarize_context_three_requests_and_instructions() { body.contains("\"text\":\"hello world\"") && !body.contains("You have exceeded the maximum number of tokens") }; - mount_sse_once(&server, first_matcher, sse1).await; + mount_sse_once_match(&server, first_matcher, sse1).await; let second_matcher = |req: &wiremock::Request| { let body = std::str::from_utf8(&req.body).unwrap_or(""); body.contains("You have exceeded the maximum number of tokens") }; - mount_sse_once(&server, second_matcher, sse2).await; + mount_sse_once_match(&server, second_matcher, sse2).await; let third_matcher = |req: &wiremock::Request| { let body = std::str::from_utf8(&req.body).unwrap_or(""); body.contains(&format!("\"text\":\"{THIRD_USER_MSG}\"")) }; - mount_sse_once(&server, third_matcher, sse3).await; + mount_sse_once_match(&server, third_matcher, sse3).await; // Build config pointing to the mock server and spawn Codex. let model_provider = ModelProviderInfo { diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index 73ad091fc9..690e1aab4d 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -25,7 +25,7 @@ use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use core_test_support::load_default_config_for_test; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; -use core_test_support::responses::mount_sse_once; +use core_test_support::responses::mount_sse_once_match; use core_test_support::responses::sse; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; @@ -702,13 +702,13 @@ async fn mount_initial_flow(server: &MockServer) { && !body.contains("\"text\":\"AFTER_RESUME\"") && !body.contains("\"text\":\"AFTER_FORK\"") }; - mount_sse_once(server, match_first, sse1).await; + mount_sse_once_match(server, match_first, sse1).await; let match_compact = |req: &wiremock::Request| { let body = std::str::from_utf8(&req.body).unwrap_or(""); body.contains("You have exceeded the maximum number of tokens") }; - mount_sse_once(server, match_compact, sse2).await; + mount_sse_once_match(server, match_compact, sse2).await; let match_after_compact = |req: &wiremock::Request| { let body = std::str::from_utf8(&req.body).unwrap_or(""); @@ -716,19 +716,19 @@ async fn mount_initial_flow(server: &MockServer) { && !body.contains("\"text\":\"AFTER_RESUME\"") && !body.contains("\"text\":\"AFTER_FORK\"") }; - mount_sse_once(server, match_after_compact, sse3).await; + mount_sse_once_match(server, match_after_compact, sse3).await; let match_after_resume = |req: &wiremock::Request| { let body = std::str::from_utf8(&req.body).unwrap_or(""); body.contains("\"text\":\"AFTER_RESUME\"") }; - mount_sse_once(server, match_after_resume, sse4).await; + mount_sse_once_match(server, match_after_resume, sse4).await; let match_after_fork = |req: &wiremock::Request| { let body = std::str::from_utf8(&req.body).unwrap_or(""); body.contains("\"text\":\"AFTER_FORK\"") }; - mount_sse_once(server, match_after_fork, sse5).await; + mount_sse_once_match(server, match_after_fork, sse5).await; } async fn mount_second_compact_flow(server: &MockServer) { @@ -743,13 +743,13 @@ async fn mount_second_compact_flow(server: &MockServer) { body.contains("You have exceeded the maximum number of tokens") && body.contains("AFTER_FORK") }; - mount_sse_once(server, match_second_compact, sse6).await; + mount_sse_once_match(server, match_second_compact, sse6).await; let match_after_second_resume = |req: &wiremock::Request| { let body = std::str::from_utf8(&req.body).unwrap_or(""); body.contains(&format!("\"text\":\"{AFTER_SECOND_RESUME}\"")) }; - mount_sse_once(server, match_after_second_resume, sse7).await; + mount_sse_once_match(server, match_after_second_resume, sse7).await; } async fn start_test_conversation( diff --git a/codex-rs/core/tests/suite/json_result.rs b/codex-rs/core/tests/suite/json_result.rs index 90d9223208..e8d91fae8b 100644 --- a/codex-rs/core/tests/suite/json_result.rs +++ b/codex-rs/core/tests/suite/json_result.rs @@ -67,7 +67,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { && format.get("strict") == Some(&serde_json::Value::Bool(true)) && format.get("schema") == Some(&expected_schema) }; - responses::mount_sse_once(&server, match_json_text_param, sse1).await; + responses::mount_sse_once_match(&server, match_json_text_param, sse1).await; let TestCodex { codex, cwd, .. } = test_codex().build(&server).await?; diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index 5dd2cb673e..b3a90ff339 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -12,6 +12,7 @@ mod fork_conversation; mod json_result; mod live_cli; mod model_overrides; +mod otel; mod prompt_caching; mod review; mod rmcp_client; diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs new file mode 100644 index 0000000000..2e7fe31794 --- /dev/null +++ b/codex-rs/core/tests/suite/otel.rs @@ -0,0 +1,1196 @@ +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::InputItem; +use codex_protocol::protocol::Op; +use codex_protocol::protocol::ReviewDecision; +use codex_protocol::protocol::SandboxPolicy; +use core_test_support::responses::ev_assistant_message; +use core_test_support::responses::ev_completed; +use core_test_support::responses::ev_custom_tool_call; +use core_test_support::responses::ev_function_call; +use core_test_support::responses::mount_sse; +use core_test_support::responses::mount_sse_once; +use core_test_support::responses::sse; +use core_test_support::responses::start_mock_server; +use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event_with_timeout; +use std::time::Duration; +use tracing_test::traced_test; + +use core_test_support::responses::ev_local_shell_call; + +#[tokio::test] +#[traced_test] +async fn responses_api_emits_api_request_event() { + let server = start_mock_server().await; + + mount_sse_once(&server, sse(vec![ev_completed("done")])).await; + + let TestCodex { codex, .. } = test_codex().build(&server).await.unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TaskComplete(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| line.contains("codex.api_request")) + .map(|_| Ok(())) + .unwrap_or_else(|| Err("expected codex.api_request event".to_string())) + }); + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| line.contains("codex.conversation_starts")) + .map(|_| Ok(())) + .unwrap_or_else(|| Err("expected codex.conversation_starts event".to_string())) + }); +} + +#[tokio::test] +#[traced_test] +async fn process_sse_emits_tracing_for_output_item() { + let server = start_mock_server().await; + + mount_sse_once( + &server, + sse(vec![ev_assistant_message("id1", "hi"), ev_completed("id2")]), + ) + .await; + + let TestCodex { codex, .. } = test_codex().build(&server).await.unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TaskComplete(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| { + line.contains("codex.sse_event") + && line.contains("event.kind=response.output_item.done") + }) + .map(|_| Ok(())) + .unwrap_or(Err("missing response.output_item.done event".to_string())) + }); +} + +#[tokio::test] +#[traced_test] +async fn process_sse_emits_failed_event_on_parse_error() { + let server = start_mock_server().await; + + mount_sse_once(&server, "data: not-json\n\n".to_string()).await; + + let TestCodex { codex, .. } = test_codex() + .with_config(move |config| { + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TaskComplete(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| { + line.contains("codex.sse_event") + && line.contains("error.message") + && line.contains("expected ident at line 1 column 2") + }) + .map(|_| Ok(())) + .unwrap_or(Err("missing codex.sse_event".to_string())) + }); +} + +#[tokio::test] +#[traced_test] +async fn process_sse_records_failed_event_when_stream_closes_without_completed() { + let server = start_mock_server().await; + + mount_sse_once(&server, sse(vec![ev_assistant_message("id", "hi")])).await; + + let TestCodex { codex, .. } = test_codex() + .with_config(move |config| { + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TaskComplete(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| { + line.contains("codex.sse_event") + && line.contains("error.message") + && line.contains("stream closed before response.completed") + }) + .map(|_| Ok(())) + .unwrap_or(Err("missing codex.sse_event".to_string())) + }); +} + +#[tokio::test] +#[traced_test] +async fn process_sse_failed_event_records_response_error_message() { + let server = start_mock_server().await; + + mount_sse_once( + &server, + sse(vec![serde_json::json!({ + "type": "response.failed", + "response": { + "error": { + "message": "boom", + "code": "bad" + } + } + })]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(move |config| { + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TaskComplete(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| { + line.contains("codex.sse_event") + && line.contains("event.kind=response.failed") + && line.contains("error.message") + && line.contains("boom") + }) + .map(|_| Ok(())) + .unwrap_or(Err("missing codex.sse_event".to_string())) + }); +} + +#[tokio::test] +#[traced_test] +async fn process_sse_failed_event_logs_parse_error() { + let server = start_mock_server().await; + + mount_sse_once( + &server, + sse(vec![serde_json::json!({ + "type": "response.failed", + "response": { + "error": "not-an-object" + } + })]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(move |config| { + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TaskComplete(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| { + line.contains("codex.sse_event") && line.contains("event.kind=response.failed") + }) + .map(|_| Ok(())) + .unwrap_or(Err("missing codex.sse_event".to_string())) + }); +} + +#[tokio::test] +#[traced_test] +async fn process_sse_failed_event_logs_missing_error() { + let server = start_mock_server().await; + + mount_sse_once( + &server, + sse(vec![serde_json::json!({ + "type": "response.failed", + "response": {} + })]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(move |config| { + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TaskComplete(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| { + line.contains("codex.sse_event") && line.contains("event.kind=response.failed") + }) + .map(|_| Ok(())) + .unwrap_or(Err("missing codex.sse_event".to_string())) + }); +} + +#[tokio::test] +#[traced_test] +async fn process_sse_failed_event_logs_response_completed_parse_error() { + let server = start_mock_server().await; + + mount_sse_once( + &server, + sse(vec![serde_json::json!({ + "type": "response.completed", + "response": {} + })]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(move |config| { + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TaskComplete(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| { + line.contains("codex.sse_event") + && line.contains("event.kind=response.completed") + && line.contains("error.message") + && line.contains("failed to parse ResponseCompleted") + }) + .map(|_| Ok(())) + .unwrap_or(Err("missing codex.sse_event".to_string())) + }); +} + +#[tokio::test] +#[traced_test] +async fn process_sse_emits_completed_telemetry() { + let server = start_mock_server().await; + + mount_sse_once( + &server, + sse(vec![serde_json::json!({ + "type": "response.completed", + "response": { + "id": "resp1", + "usage": { + "input_tokens": 3, + "input_tokens_details": { "cached_tokens": 1 }, + "output_tokens": 5, + "output_tokens_details": { "reasoning_tokens": 2 }, + "total_tokens": 9 + } + } + })]), + ) + .await; + + let TestCodex { codex, .. } = test_codex().build(&server).await.unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TaskComplete(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + lines + .iter() + .find(|line| { + line.contains("codex.sse_event") + && line.contains("event.kind=response.completed") + && line.contains("input_token_count=3") + && line.contains("output_token_count=5") + && line.contains("cached_token_count=1") + && line.contains("reasoning_token_count=2") + && line.contains("tool_token_count=9") + }) + .map(|_| Ok(())) + .unwrap_or(Err("missing response.completed telemetry".to_string())) + }); +} + +#[tokio::test] +#[traced_test] +async fn handle_response_item_records_tool_result_for_custom_tool_call() { + let server = start_mock_server().await; + + mount_sse( + &server, + sse(vec![ + ev_custom_tool_call( + "custom-tool-call", + "unsupported_tool", + "{\"key\":\"value\"}", + ), + ev_completed("done"), + ]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(move |config| { + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TokenCount(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + let line = lines + .iter() + .find(|line| { + line.contains("codex.tool_result") && line.contains("call_id=custom-tool-call") + }) + .ok_or_else(|| "missing codex.tool_result event".to_string())?; + + if !line.contains("tool_name=unsupported_tool") { + return Err("missing tool_name field".to_string()); + } + if !line.contains("arguments={\"key\":\"value\"}") { + return Err("missing arguments field".to_string()); + } + if !line.contains("output=unsupported custom tool call: unsupported_tool") { + return Err("missing output field".to_string()); + } + if !line.contains("success=false") { + return Err("missing success field".to_string()); + } + + Ok(()) + }); +} + +#[tokio::test] +#[traced_test] +async fn handle_response_item_records_tool_result_for_function_call() { + let server = start_mock_server().await; + + mount_sse( + &server, + sse(vec![ + ev_function_call("function-call", "nonexistent", "{\"value\":1}"), + ev_completed("done"), + ]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(move |config| { + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TokenCount(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + let line = lines + .iter() + .find(|line| { + line.contains("codex.tool_result") && line.contains("call_id=function-call") + }) + .ok_or_else(|| "missing codex.tool_result event".to_string())?; + + if !line.contains("tool_name=nonexistent") { + return Err("missing tool_name field".to_string()); + } + if !line.contains("arguments={\"value\":1}") { + return Err("missing arguments field".to_string()); + } + if !line.contains("output=unsupported call: nonexistent") { + return Err("missing output field".to_string()); + } + if !line.contains("success=false") { + return Err("missing success field".to_string()); + } + + Ok(()) + }); +} + +#[tokio::test] +#[traced_test] +async fn handle_response_item_records_tool_result_for_local_shell_missing_ids() { + let server = start_mock_server().await; + + mount_sse( + &server, + sse(vec![ + serde_json::json!({ + "type": "response.output_item.done", + "item": { + "type": "local_shell_call", + "status": "completed", + "action": { + "type": "exec", + "command": vec!["/bin/echo", "hello"], + } + } + }), + ev_completed("done"), + ]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(move |config| { + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TokenCount(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + let line = lines + .iter() + .find(|line| { + line.contains("codex.tool_result") + && line.contains(&"tool_name=local_shell".to_string()) + && line.contains("output=LocalShellCall without call_id or id") + }) + .ok_or_else(|| "missing codex.tool_result event".to_string())?; + + if !line.contains("success=false") { + return Err("missing success field".to_string()); + } + + Ok(()) + }); +} + +#[cfg(target_os = "macos")] +#[tokio::test] +#[traced_test] +async fn handle_response_item_records_tool_result_for_local_shell_call() { + let server = start_mock_server().await; + + mount_sse( + &server, + sse(vec![ + ev_local_shell_call("shell-call", "completed", vec!["/bin/echo", "shell"]), + ev_completed("done"), + ]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(move |config| { + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TokenCount(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(|lines: &[&str]| { + let line = lines + .iter() + .find(|line| line.contains("codex.tool_result") && line.contains("call_id=shell-call")) + .ok_or_else(|| "missing codex.tool_result event".to_string())?; + + if !line.contains("tool_name=local_shell") { + return Err("missing tool_name field".to_string()); + } + if !line.contains("arguments=/bin/echo shell") { + return Err("missing arguments field".to_string()); + } + let output_idx = line + .find("output=") + .ok_or_else(|| "missing output field".to_string())?; + if line[output_idx + "output=".len()..].is_empty() { + return Err("empty output field".to_string()); + } + if !line.contains("success=false") { + return Err("missing success field".to_string()); + } + + Ok(()) + }); +} + +fn tool_decision_assertion<'a>( + call_id: &'a str, + expected_decision: &'a str, + expected_source: &'a str, +) -> impl Fn(&[&str]) -> Result<(), String> + 'a { + let call_id = call_id.to_string(); + let expected_decision = expected_decision.to_string(); + let expected_source = expected_source.to_string(); + + move |lines: &[&str]| { + let line = lines + .iter() + .find(|line| { + line.contains("codex.tool_decision") && line.contains(&format!("call_id={call_id}")) + }) + .ok_or_else(|| format!("missing codex.tool_decision event for {call_id}"))?; + + let lower = line.to_lowercase(); + if !lower.contains("tool_name=local_shell") { + return Err("missing tool_name for local_shell".to_string()); + } + if !lower.contains(&format!("decision={expected_decision}")) { + return Err(format!("unexpected decision for {call_id}")); + } + if !lower.contains(&format!("source={expected_source}")) { + return Err(format!("unexpected source for {expected_source}")); + } + + Ok(()) + } +} + +#[tokio::test] +#[traced_test] +async fn handle_container_exec_autoapprove_from_config_records_tool_decision() { + let server = start_mock_server().await; + mount_sse( + &server, + sse(vec![ + ev_local_shell_call("auto_config_call", "completed", vec!["/bin/echo", "hello"]), + ev_completed("done"), + ]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(|config| { + config.approval_policy = AskForApproval::OnRequest; + config.sandbox_policy = SandboxPolicy::DangerFullAccess; + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TokenCount(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(tool_decision_assertion( + "auto_config_call", + "approved", + "config", + )); +} + +#[tokio::test] +#[traced_test] +async fn handle_container_exec_user_approved_records_tool_decision() { + let server = start_mock_server().await; + mount_sse( + &server, + sse(vec![ + ev_local_shell_call( + "user_approved_call", + "completed", + vec!["/bin/echo", "approved"], + ), + ev_completed("done"), + ]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(|config| { + config.approval_policy = AskForApproval::UnlessTrusted; + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "approved".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::ExecApprovalRequest(_)), + Duration::from_secs(5), + ) + .await; + + codex + .submit(Op::ExecApproval { + id: "0".into(), + decision: ReviewDecision::Approved, + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TokenCount(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(tool_decision_assertion( + "user_approved_call", + "approved", + "user", + )); +} + +#[tokio::test] +#[traced_test] +async fn handle_container_exec_user_approved_for_session_records_tool_decision() { + let server = start_mock_server().await; + + mount_sse( + &server, + sse(vec![ + ev_local_shell_call( + "user_approved_session_call", + "completed", + vec!["/bin/echo", "persist"], + ), + ev_completed("done"), + ]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(|config| { + config.approval_policy = AskForApproval::UnlessTrusted; + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "persist".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::ExecApprovalRequest(_)), + Duration::from_secs(5), + ) + .await; + + codex + .submit(Op::ExecApproval { + id: "0".into(), + decision: ReviewDecision::ApprovedForSession, + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TokenCount(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(tool_decision_assertion( + "user_approved_session_call", + "approvedforsession", + "user", + )); +} + +#[tokio::test] +#[traced_test] +async fn handle_sandbox_error_user_approves_retry_records_tool_decision() { + let server = start_mock_server().await; + + mount_sse( + &server, + sse(vec![ + ev_local_shell_call( + "sandbox_retry_call", + "completed", + vec!["/bin/echo", "retry"], + ), + ev_completed("done"), + ]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(|config| { + config.approval_policy = AskForApproval::UnlessTrusted; + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "retry".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::ExecApprovalRequest(_)), + Duration::from_secs(5), + ) + .await; + + codex + .submit(Op::ExecApproval { + id: "0".into(), + decision: ReviewDecision::Approved, + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TokenCount(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(tool_decision_assertion( + "sandbox_retry_call", + "approved", + "user", + )); +} + +#[tokio::test] +#[traced_test] +async fn handle_container_exec_user_denies_records_tool_decision() { + let server = start_mock_server().await; + + mount_sse( + &server, + sse(vec![ + ev_local_shell_call("user_denied_call", "completed", vec!["/bin/echo", "deny"]), + ev_completed("done"), + ]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(|config| { + config.approval_policy = AskForApproval::UnlessTrusted; + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "deny".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::ExecApprovalRequest(_)), + Duration::from_secs(5), + ) + .await; + + codex + .submit(Op::ExecApproval { + id: "0".into(), + decision: ReviewDecision::Denied, + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TokenCount(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(tool_decision_assertion( + "user_denied_call", + "denied", + "user", + )); +} + +#[tokio::test] +#[traced_test] +async fn handle_sandbox_error_user_approves_for_session_records_tool_decision() { + let server = start_mock_server().await; + + mount_sse( + &server, + sse(vec![ + ev_local_shell_call( + "sandbox_session_call", + "completed", + vec!["/bin/echo", "persist"], + ), + ev_completed("done"), + ]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(|config| { + config.approval_policy = AskForApproval::UnlessTrusted; + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "persist".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::ExecApprovalRequest(_)), + Duration::from_secs(5), + ) + .await; + + codex + .submit(Op::ExecApproval { + id: "0".into(), + decision: ReviewDecision::ApprovedForSession, + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TokenCount(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(tool_decision_assertion( + "sandbox_session_call", + "approvedforsession", + "user", + )); +} + +#[tokio::test] +#[traced_test] +async fn handle_sandbox_error_user_denies_records_tool_decision() { + let server = start_mock_server().await; + + mount_sse( + &server, + sse(vec![ + ev_local_shell_call("sandbox_deny_call", "completed", vec!["/bin/echo", "deny"]), + ev_completed("done"), + ]), + ) + .await; + + let TestCodex { codex, .. } = test_codex() + .with_config(|config| { + config.approval_policy = AskForApproval::UnlessTrusted; + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "deny".into(), + }], + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::ExecApprovalRequest(_)), + Duration::from_secs(5), + ) + .await; + + codex + .submit(Op::ExecApproval { + id: "0".into(), + decision: ReviewDecision::Denied, + }) + .await + .unwrap(); + + wait_for_event_with_timeout( + &codex, + |ev| matches!(ev, EventMsg::TokenCount(_)), + Duration::from_secs(5), + ) + .await; + + logs_assert(tool_decision_assertion( + "sandbox_deny_call", + "denied", + "user", + )); +} diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index aed84ec603..bd1682406d 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -12,7 +12,7 @@ use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; use codex_protocol::config_types::ReasoningSummary; use core_test_support::responses; -use core_test_support::responses::mount_sse_once; +use core_test_support::responses::mount_sse_once_match; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; @@ -36,7 +36,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { let server_name = "rmcp"; let tool_name = format!("{server_name}__echo"); - mount_sse_once( + mount_sse_once_match( &server, any(), responses::sse(vec![ @@ -49,7 +49,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { ]), ) .await; - mount_sse_once( + mount_sse_once_match( &server, any(), responses::sse(vec![ @@ -173,7 +173,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { let server_name = "rmcp_http"; let tool_name = format!("{server_name}__echo"); - mount_sse_once( + mount_sse_once_match( &server, any(), responses::sse(vec![ @@ -186,7 +186,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { ]), ) .await; - mount_sse_once( + mount_sse_once_match( &server, any(), responses::sse(vec![ diff --git a/codex-rs/core/tests/suite/user_notification.rs b/codex-rs/core/tests/suite/user_notification.rs index c162239407..2ad87c7b6c 100644 --- a/codex-rs/core/tests/suite/user_notification.rs +++ b/codex-rs/core/tests/suite/user_notification.rs @@ -28,7 +28,7 @@ async fn summarize_context_three_requests_and_instructions() -> anyhow::Result<( let sse1 = sse(vec![ev_assistant_message("m1", "Done"), ev_completed("r1")]); - responses::mount_sse_once(&server, any(), sse1).await; + responses::mount_sse_once_match(&server, any(), sse1).await; let notify_dir = TempDir::new()?; // write a script to the notify that touches a file next to it diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index 8603e8fd89..e37865b25c 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -40,12 +40,14 @@ tokio = { workspace = true, features = [ ] } tracing = { workspace = true, features = ["log"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } +opentelemetry-appender-tracing = { workspace = true } ts-rs = { workspace = true, features = [ "uuid-impl", "serde-json-impl", "no-serde-warnings", ] } + [dev-dependencies] assert_cmd = { workspace = true } core_test_support = { workspace = true } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index da23fb1b23..de468493ef 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -5,10 +5,6 @@ pub mod event_processor_with_json_output; pub mod exec_events; pub mod experimental_event_processor_with_json_output; -use std::io::IsTerminal; -use std::io::Read; -use std::path::PathBuf; - pub use cli::Cli; use codex_core::AuthManager; use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID; @@ -26,17 +22,22 @@ use codex_core::protocol::TaskCompleteEvent; use codex_ollama::DEFAULT_OSS_MODEL; use codex_protocol::config_types::SandboxMode; use event_processor_with_human_output::EventProcessorWithHumanOutput; +use event_processor_with_json_output::EventProcessorWithJsonOutput; use experimental_event_processor_with_json_output::ExperimentalEventProcessorWithJsonOutput; +use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; use serde_json::Value; +use std::io::IsTerminal; +use std::io::Read; +use std::path::PathBuf; use tracing::debug; use tracing::error; use tracing::info; use tracing_subscriber::EnvFilter; +use tracing_subscriber::prelude::*; use crate::cli::Command as ExecCommand; use crate::event_processor::CodexStatus; use crate::event_processor::EventProcessor; -use crate::event_processor_with_json_output::EventProcessorWithJsonOutput; use codex_core::find_conversation_path_by_id_str; pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> anyhow::Result<()> { @@ -114,19 +115,18 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any ), }; - // TODO(mbolin): Take a more thoughtful approach to logging. + // Build fmt layer (existing logging) to compose with OTEL layer. let default_level = "error"; - let _ = tracing_subscriber::fmt() - // Fallback to the `default_level` log filter if the environment - // variable is not set _or_ contains an invalid value - .with_env_filter( - EnvFilter::try_from_default_env() - .or_else(|_| EnvFilter::try_new(default_level)) - .unwrap_or_else(|_| EnvFilter::new(default_level)), - ) + + // Build env_filter separately and attach via with_filter. + let env_filter = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new(default_level)) + .unwrap_or_else(|_| EnvFilter::new(default_level)); + + let fmt_layer = tracing_subscriber::fmt::layer() .with_ansi(stderr_with_ansi) .with_writer(std::io::stderr) - .try_init(); + .with_filter(env_filter); let sandbox_mode = if full_auto { Some(SandboxMode::WorkspaceWrite) @@ -182,6 +182,31 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any }; let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?; + + let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION")); + + #[allow(clippy::print_stderr)] + let otel = match otel { + Ok(otel) => otel, + Err(e) => { + eprintln!("Could not create otel exporter: {e}"); + std::process::exit(1); + } + }; + + if let Some(provider) = otel.as_ref() { + let otel_layer = OpenTelemetryTracingBridge::new(&provider.logger).with_filter( + tracing_subscriber::filter::filter_fn(codex_core::otel_init::codex_export_filter), + ); + + let _ = tracing_subscriber::registry() + .with(fmt_layer) + .with(otel_layer) + .try_init(); + } else { + let _ = tracing_subscriber::registry().with(fmt_layer).try_init(); + } + let mut event_processor: Box = match (json_mode, experimental_json) { (_, true) => Box::new(ExperimentalEventProcessorWithJsonOutput::new( last_message_file.clone(), diff --git a/codex-rs/exec/tests/suite/output_schema.rs b/codex-rs/exec/tests/suite/output_schema.rs index 03a04d0588..d9fcb763e7 100644 --- a/codex-rs/exec/tests/suite/output_schema.rs +++ b/codex-rs/exec/tests/suite/output_schema.rs @@ -31,7 +31,7 @@ async fn exec_includes_output_schema_in_request() -> anyhow::Result<()> { responses::ev_assistant_message("m1", "fixture hello"), responses::ev_completed("resp1"), ]); - responses::mount_sse_once(&server, any(), body).await; + responses::mount_sse_once_match(&server, any(), body).await; test.cmd_with_server(&server) .arg("--skip-git-repo-check") diff --git a/codex-rs/exec/tests/suite/server_error_exit.rs b/codex-rs/exec/tests/suite/server_error_exit.rs index 6b6358f0f4..051306cedd 100644 --- a/codex-rs/exec/tests/suite/server_error_exit.rs +++ b/codex-rs/exec/tests/suite/server_error_exit.rs @@ -21,7 +21,7 @@ async fn exits_non_zero_when_server_reports_error() -> anyhow::Result<()> { "error": {"code": "rate_limit_exceeded", "message": "synthetic server error"} } })]); - responses::mount_sse_once(&server, any(), body).await; + responses::mount_sse_once_match(&server, any(), body).await; test.cmd_with_server(&server) .arg("--skip-git-repo-check") diff --git a/codex-rs/otel/Cargo.toml b/codex-rs/otel/Cargo.toml new file mode 100644 index 0000000000..610423f9cc --- /dev/null +++ b/codex-rs/otel/Cargo.toml @@ -0,0 +1,39 @@ +[package] +edition = "2024" +name = "codex-otel" +version = { workspace = true } + +[lib] +name = "codex_otel" +path = "src/lib.rs" +doctest = false + +[lints] +workspace = true + +[features] +# Compile-time gate for OTLP support; disabled by default. +# Downstream crates can enable via `features = ["otel"]`. +default = [] +otel = [ + "opentelemetry", + "opentelemetry_sdk", + "opentelemetry-otlp", + "tonic", +] + +[dependencies] +codex-protocol = { path = "../protocol" } +chrono = { workspace = true } +tracing = { workspace = true } +opentelemetry = { workspace = true, features = ["logs"], optional = true } +opentelemetry_sdk = { workspace = true, features = ["logs", "rt-tokio"], optional = true } +opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "http-proto", "http-json", "reqwest", "reqwest-rustls"], optional = true } +opentelemetry-semantic-conventions = { workspace = true } +tonic = { workspace = true, optional = true } +serde = { workspace = true, features = ["derive"] } +strum_macros = { workspace = true } +reqwest = { workspace = true } +eventsource-stream = { workspace = true } +tokio = { workspace = true } +serde_json = { workspace = true } \ No newline at end of file diff --git a/codex-rs/otel/src/config.rs b/codex-rs/otel/src/config.rs new file mode 100644 index 0000000000..77063ed09d --- /dev/null +++ b/codex-rs/otel/src/config.rs @@ -0,0 +1,33 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub struct OtelSettings { + pub environment: String, + pub service_name: String, + pub service_version: String, + pub codex_home: PathBuf, + pub exporter: OtelExporter, +} + +#[derive(Clone, Debug)] +pub enum OtelHttpProtocol { + /// HTTP protocol with binary protobuf + Binary, + /// HTTP protocol with JSON payload + Json, +} + +#[derive(Clone, Debug)] +pub enum OtelExporter { + None, + OtlpGrpc { + endpoint: String, + headers: HashMap, + }, + OtlpHttp { + endpoint: String, + headers: HashMap, + protocol: OtelHttpProtocol, + }, +} diff --git a/codex-rs/otel/src/lib.rs b/codex-rs/otel/src/lib.rs new file mode 100644 index 0000000000..3fdb6f7413 --- /dev/null +++ b/codex-rs/otel/src/lib.rs @@ -0,0 +1,26 @@ +pub mod config; + +pub mod otel_event_manager; +#[cfg(feature = "otel")] +pub mod otel_provider; + +#[cfg(not(feature = "otel"))] +mod imp { + use reqwest::header::HeaderMap; + use tracing::Span; + + pub struct OtelProvider; + + impl OtelProvider { + pub fn from(_settings: &crate::config::OtelSettings) -> Option { + None + } + + pub fn headers(_span: &Span) -> HeaderMap { + HeaderMap::new() + } + } +} + +#[cfg(not(feature = "otel"))] +pub use imp::OtelProvider; diff --git a/codex-rs/otel/src/otel_event_manager.rs b/codex-rs/otel/src/otel_event_manager.rs new file mode 100644 index 0000000000..87ed6bb535 --- /dev/null +++ b/codex-rs/otel/src/otel_event_manager.rs @@ -0,0 +1,459 @@ +use chrono::SecondsFormat; +use chrono::Utc; +use codex_protocol::config_types::ReasoningEffort; +use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::mcp_protocol::AuthMode; +use codex_protocol::mcp_protocol::ConversationId; +use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::InputItem; +use codex_protocol::protocol::ReviewDecision; +use codex_protocol::protocol::SandboxPolicy; +use eventsource_stream::Event as StreamEvent; +use eventsource_stream::EventStreamError as StreamError; +use reqwest::Error; +use reqwest::Response; +use serde::Serialize; +use std::fmt::Display; +use std::time::Duration; +use std::time::Instant; +use strum_macros::Display; +use tokio::time::error::Elapsed; + +#[derive(Debug, Clone, Serialize, Display)] +#[serde(rename_all = "snake_case")] +pub enum ToolDecisionSource { + Config, + User, +} + +#[derive(Debug, Clone)] +pub struct OtelEventMetadata { + conversation_id: ConversationId, + auth_mode: Option, + account_id: Option, + model: String, + slug: String, + log_user_prompts: bool, + app_version: &'static str, + terminal_type: String, +} + +#[derive(Debug, Clone)] +pub struct OtelEventManager { + metadata: OtelEventMetadata, +} + +impl OtelEventManager { + pub fn new( + conversation_id: ConversationId, + model: &str, + slug: &str, + account_id: Option, + auth_mode: Option, + log_user_prompts: bool, + terminal_type: String, + ) -> OtelEventManager { + Self { + metadata: OtelEventMetadata { + conversation_id, + auth_mode: auth_mode.map(|m| m.to_string()), + account_id, + model: model.to_owned(), + slug: slug.to_owned(), + log_user_prompts, + app_version: env!("CARGO_PKG_VERSION"), + terminal_type, + }, + } + } + + pub fn with_model(&self, model: &str, slug: &str) -> Self { + let mut manager = self.clone(); + manager.metadata.model = model.to_owned(); + manager.metadata.slug = slug.to_owned(); + manager + } + + #[allow(clippy::too_many_arguments)] + pub fn conversation_starts( + &self, + provider_name: &str, + reasoning_effort: Option, + reasoning_summary: ReasoningSummary, + context_window: Option, + max_output_tokens: Option, + auto_compact_token_limit: Option, + approval_policy: AskForApproval, + sandbox_policy: SandboxPolicy, + mcp_servers: Vec<&str>, + active_profile: Option, + ) { + tracing::event!( + tracing::Level::INFO, + event.name = "codex.conversation_starts", + event.timestamp = %timestamp(), + conversation.id = %self.metadata.conversation_id, + app.version = %self.metadata.app_version, + auth_mode = self.metadata.auth_mode, + user.account_id = self.metadata.account_id, + terminal.type = %self.metadata.terminal_type, + model = %self.metadata.model, + slug = %self.metadata.slug, + provider_name = %provider_name, + reasoning_effort = reasoning_effort.map(|e| e.to_string()), + reasoning_summary = %reasoning_summary, + context_window = context_window, + max_output_tokens = max_output_tokens, + auto_compact_token_limit = auto_compact_token_limit, + approval_policy = %approval_policy, + sandbox_policy = %sandbox_policy, + mcp_servers = mcp_servers.join(", "), + active_profile = active_profile, + ) + } + + pub async fn log_request(&self, attempt: u64, f: F) -> Result + where + F: FnOnce() -> Fut, + Fut: Future>, + { + let start = std::time::Instant::now(); + let response = f().await; + let duration = start.elapsed(); + + let (status, error) = match &response { + Ok(response) => (Some(response.status().as_u16()), None), + Err(error) => (error.status().map(|s| s.as_u16()), Some(error.to_string())), + }; + + tracing::event!( + tracing::Level::INFO, + event.name = "codex.api_request", + event.timestamp = %timestamp(), + conversation.id = %self.metadata.conversation_id, + app.version = %self.metadata.app_version, + auth_mode = self.metadata.auth_mode, + user.account_id = self.metadata.account_id, + terminal.type = %self.metadata.terminal_type, + model = %self.metadata.model, + slug = %self.metadata.slug, + duration_ms = %duration.as_millis(), + http.response.status_code = status, + error.message = error, + attempt = attempt, + ); + + response + } + + pub async fn log_sse_event( + &self, + next: Next, + ) -> Result>>, Elapsed> + where + Next: FnOnce() -> Fut, + Fut: Future>>, Elapsed>>, + E: Display, + { + let start = std::time::Instant::now(); + let response = next().await; + let duration = start.elapsed(); + + match response { + Ok(Some(Ok(ref sse))) => { + if sse.data.trim() == "[DONE]" { + self.sse_event(&sse.event, duration); + } else { + match serde_json::from_str::(&sse.data) { + Ok(error) if sse.event == "response.failed" => { + self.sse_event_failed(Some(&sse.event), duration, &error); + } + Ok(content) if sse.event == "response.output_item.done" => { + match serde_json::from_value::(content) { + Ok(_) => self.sse_event(&sse.event, duration), + Err(_) => { + self.sse_event_failed( + Some(&sse.event), + duration, + &"failed to parse response.output_item.done", + ); + } + }; + } + Ok(_) => { + self.sse_event(&sse.event, duration); + } + Err(error) => { + self.sse_event_failed(Some(&sse.event), duration, &error); + } + } + } + } + Ok(Some(Err(ref error))) => { + self.sse_event_failed(None, duration, error); + } + Ok(None) => {} + Err(_) => { + self.sse_event_failed(None, duration, &"idle timeout waiting for SSE"); + } + } + + response + } + + fn sse_event(&self, kind: &str, duration: Duration) { + tracing::event!( + tracing::Level::INFO, + event.name = "codex.sse_event", + event.timestamp = %timestamp(), + event.kind = %kind, + conversation.id = %self.metadata.conversation_id, + app.version = %self.metadata.app_version, + auth_mode = self.metadata.auth_mode, + user.account_id = self.metadata.account_id, + terminal.type = %self.metadata.terminal_type, + model = %self.metadata.model, + slug = %self.metadata.slug, + duration_ms = %duration.as_millis(), + ); + } + + pub fn sse_event_failed(&self, kind: Option<&String>, duration: Duration, error: &T) + where + T: Display, + { + match kind { + Some(kind) => tracing::event!( + tracing::Level::INFO, + event.name = "codex.sse_event", + event.timestamp = %timestamp(), + event.kind = %kind, + conversation.id = %self.metadata.conversation_id, + app.version = %self.metadata.app_version, + auth_mode = self.metadata.auth_mode, + user.account_id = self.metadata.account_id, + terminal.type = %self.metadata.terminal_type, + model = %self.metadata.model, + slug = %self.metadata.slug, + duration_ms = %duration.as_millis(), + error.message = %error, + ), + None => tracing::event!( + tracing::Level::INFO, + event.name = "codex.sse_event", + event.timestamp = %timestamp(), + conversation.id = %self.metadata.conversation_id, + app.version = %self.metadata.app_version, + auth_mode = self.metadata.auth_mode, + user.account_id = self.metadata.account_id, + terminal.type = %self.metadata.terminal_type, + model = %self.metadata.model, + slug = %self.metadata.slug, + duration_ms = %duration.as_millis(), + error.message = %error, + ), + } + } + + pub fn see_event_completed_failed(&self, error: &T) + where + T: Display, + { + tracing::event!( + tracing::Level::INFO, + event.name = "codex.sse_event", + event.kind = %"response.completed", + event.timestamp = %timestamp(), + conversation.id = %self.metadata.conversation_id, + app.version = %self.metadata.app_version, + auth_mode = self.metadata.auth_mode, + user.account_id = self.metadata.account_id, + terminal.type = %self.metadata.terminal_type, + model = %self.metadata.model, + slug = %self.metadata.slug, + error.message = %error, + ) + } + + pub fn sse_event_completed( + &self, + input_token_count: u64, + output_token_count: u64, + cached_token_count: Option, + reasoning_token_count: Option, + tool_token_count: u64, + ) { + tracing::event!( + tracing::Level::INFO, + event.name = "codex.sse_event", + event.timestamp = %timestamp(), + event.kind = %"response.completed", + conversation.id = %self.metadata.conversation_id, + app.version = %self.metadata.app_version, + auth_mode = self.metadata.auth_mode, + user.account_id = self.metadata.account_id, + terminal.type = %self.metadata.terminal_type, + model = %self.metadata.model, + slug = %self.metadata.slug, + input_token_count = %input_token_count, + output_token_count = %output_token_count, + cached_token_count = cached_token_count, + reasoning_token_count = reasoning_token_count, + tool_token_count = %tool_token_count, + ); + } + + pub fn user_prompt(&self, items: &[InputItem]) { + let prompt = items + .iter() + .flat_map(|item| match item { + InputItem::Text { text } => Some(text.as_str()), + _ => None, + }) + .collect::(); + + let prompt_to_log = if self.metadata.log_user_prompts { + prompt.as_str() + } else { + "[REDACTED]" + }; + + tracing::event!( + tracing::Level::INFO, + event.name = "codex.user_prompt", + event.timestamp = %timestamp(), + conversation.id = %self.metadata.conversation_id, + app.version = %self.metadata.app_version, + auth_mode = self.metadata.auth_mode, + user.account_id = self.metadata.account_id, + terminal.type = %self.metadata.terminal_type, + model = %self.metadata.model, + slug = %self.metadata.slug, + prompt_length = %prompt.chars().count(), + prompt = %prompt_to_log, + ); + } + + pub fn tool_decision( + &self, + tool_name: &str, + call_id: &str, + decision: ReviewDecision, + source: ToolDecisionSource, + ) { + tracing::event!( + tracing::Level::INFO, + event.name = "codex.tool_decision", + event.timestamp = %timestamp(), + conversation.id = %self.metadata.conversation_id, + app.version = %self.metadata.app_version, + auth_mode = self.metadata.auth_mode, + user.account_id = self.metadata.account_id, + terminal.type = %self.metadata.terminal_type, + model = %self.metadata.model, + slug = %self.metadata.slug, + tool_name = %tool_name, + call_id = %call_id, + decision = %decision.to_string().to_lowercase(), + source = %source.to_string(), + ); + } + + pub async fn log_tool_result( + &self, + tool_name: &str, + call_id: &str, + arguments: &str, + f: F, + ) -> Result + where + F: FnOnce() -> Fut, + Fut: Future>, + E: Display, + { + let start = Instant::now(); + let result = f().await; + let duration = start.elapsed(); + + let (output, success) = match &result { + Ok(content) => (content, true), + Err(error) => (&error.to_string(), false), + }; + + tracing::event!( + tracing::Level::INFO, + event.name = "codex.tool_result", + event.timestamp = %timestamp(), + conversation.id = %self.metadata.conversation_id, + app.version = %self.metadata.app_version, + auth_mode = self.metadata.auth_mode, + user.account_id = self.metadata.account_id, + terminal.type = %self.metadata.terminal_type, + model = %self.metadata.model, + slug = %self.metadata.slug, + tool_name = %tool_name, + call_id = %call_id, + arguments = %arguments, + duration_ms = %duration.as_millis(), + success = %success, + output = %output, + ); + + result + } + + pub fn log_tool_failed(&self, tool_name: &str, error: &str) { + tracing::event!( + tracing::Level::INFO, + event.name = "codex.tool_result", + event.timestamp = %timestamp(), + conversation.id = %self.metadata.conversation_id, + app.version = %self.metadata.app_version, + auth_mode = self.metadata.auth_mode, + user.account_id = self.metadata.account_id, + terminal.type = %self.metadata.terminal_type, + model = %self.metadata.model, + slug = %self.metadata.slug, + tool_name = %tool_name, + duration_ms = %Duration::ZERO.as_millis(), + success = %false, + output = %error, + ); + } + + pub fn tool_result( + &self, + tool_name: &str, + call_id: &str, + arguments: &str, + duration: Duration, + success: bool, + output: &str, + ) { + let success_str = if success { "true" } else { "false" }; + + tracing::event!( + tracing::Level::INFO, + event.name = "codex.tool_result", + event.timestamp = %timestamp(), + conversation.id = %self.metadata.conversation_id, + app.version = %self.metadata.app_version, + auth_mode = self.metadata.auth_mode, + user.account_id = self.metadata.account_id, + terminal.type = %self.metadata.terminal_type, + model = %self.metadata.model, + slug = %self.metadata.slug, + tool_name = %tool_name, + call_id = %call_id, + arguments = %arguments, + duration_ms = %duration.as_millis(), + success = %success_str, + output = %output, + ); + } +} + +fn timestamp() -> String { + Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true) +} diff --git a/codex-rs/otel/src/otel_provider.rs b/codex-rs/otel/src/otel_provider.rs new file mode 100644 index 0000000000..222322a2ea --- /dev/null +++ b/codex-rs/otel/src/otel_provider.rs @@ -0,0 +1,103 @@ +use crate::config::OtelExporter; +use crate::config::OtelHttpProtocol; +use crate::config::OtelSettings; +use opentelemetry::KeyValue; +use opentelemetry_otlp::LogExporter; +use opentelemetry_otlp::Protocol; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_otlp::WithHttpConfig; +use opentelemetry_otlp::WithTonicConfig; +use opentelemetry_sdk::Resource; +use opentelemetry_sdk::logs::SdkLoggerProvider; +use opentelemetry_semantic_conventions as semconv; +use reqwest::header::HeaderMap; +use reqwest::header::HeaderName; +use reqwest::header::HeaderValue; +use std::error::Error; +use tonic::metadata::MetadataMap; +use tracing::debug; + +const ENV_ATTRIBUTE: &str = "env"; + +pub struct OtelProvider { + pub logger: SdkLoggerProvider, +} + +impl OtelProvider { + pub fn shutdown(&self) { + let _ = self.logger.shutdown(); + } + + pub fn from(settings: &OtelSettings) -> Result, Box> { + let resource = Resource::builder() + .with_service_name(settings.service_name.clone()) + .with_attributes(vec![ + KeyValue::new( + semconv::attribute::SERVICE_VERSION, + settings.service_version.clone(), + ), + KeyValue::new(ENV_ATTRIBUTE, settings.environment.clone()), + ]) + .build(); + + let mut builder = SdkLoggerProvider::builder().with_resource(resource); + + match &settings.exporter { + OtelExporter::None => { + debug!("No exporter enabled in OTLP settings."); + return Ok(None); + } + OtelExporter::OtlpGrpc { endpoint, headers } => { + debug!("Using OTLP Grpc exporter: {}", endpoint); + + let mut header_map = HeaderMap::new(); + for (key, value) in headers { + if let Ok(name) = HeaderName::from_bytes(key.as_bytes()) + && let Ok(val) = HeaderValue::from_str(value) + { + header_map.insert(name, val); + } + } + + let exporter = LogExporter::builder() + .with_tonic() + .with_endpoint(endpoint) + .with_metadata(MetadataMap::from_headers(header_map)) + .build()?; + + builder = builder.with_batch_exporter(exporter); + } + OtelExporter::OtlpHttp { + endpoint, + headers, + protocol, + } => { + debug!("Using OTLP Http exporter: {}", endpoint); + + let protocol = match protocol { + OtelHttpProtocol::Binary => Protocol::HttpBinary, + OtelHttpProtocol::Json => Protocol::HttpJson, + }; + + let exporter = LogExporter::builder() + .with_http() + .with_endpoint(endpoint) + .with_protocol(protocol) + .with_headers(headers.clone()) + .build()?; + + builder = builder.with_batch_exporter(exporter); + } + } + + Ok(Some(Self { + logger: builder.build(), + })) + } +} + +impl Drop for OtelProvider { + fn drop(&mut self) { + let _ = self.logger.shutdown(); + } +} diff --git a/codex-rs/protocol/src/mcp_protocol.rs b/codex-rs/protocol/src/mcp_protocol.rs index c63bf2aecc..a9f808b16e 100644 --- a/codex-rs/protocol/src/mcp_protocol.rs +++ b/codex-rs/protocol/src/mcp_protocol.rs @@ -81,7 +81,7 @@ impl GitSha { } } -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Display, TS)] #[serde(rename_all = "lowercase")] pub enum AuthMode { ApiKey, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index bf0b192548..01b4eb3a5a 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1206,7 +1206,7 @@ pub struct SessionConfiguredEvent { } /// User's decision in response to an ExecApprovalRequest. -#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, TS)] +#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Display, TS)] #[serde(rename_all = "snake_case")] pub enum ReviewDecision { /// User has approved this command and the agent should execute it. diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 51f5c23578..2e89288b02 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -77,6 +77,7 @@ tokio-stream = { workspace = true } tracing = { workspace = true, features = ["log"] } tracing-appender = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } +opentelemetry-appender-tracing = { workspace = true } unicode-segmentation = { workspace = true } unicode-width = { workspace = true } url = { workspace = true } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index e959973b22..dd3dcb9e2c 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -20,6 +20,7 @@ use codex_core::protocol::SandboxPolicy; use codex_ollama::DEFAULT_OSS_MODEL; use codex_protocol::config_types::SandboxMode; use codex_protocol::mcp_protocol::AuthMode; +use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; use std::fs::OpenOptions; use std::path::PathBuf; use tracing::error; @@ -239,7 +240,29 @@ pub async fn run_main( .map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?; } - let _ = tracing_subscriber::registry().with(file_layer).try_init(); + let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION")); + + #[allow(clippy::print_stderr)] + let otel = match otel { + Ok(otel) => otel, + Err(e) => { + eprintln!("Could not create otel exporter: {e}"); + std::process::exit(1); + } + }; + + if let Some(provider) = otel.as_ref() { + let otel_layer = OpenTelemetryTracingBridge::new(&provider.logger).with_filter( + tracing_subscriber::filter::filter_fn(codex_core::otel_init::codex_export_filter), + ); + + let _ = tracing_subscriber::registry() + .with(file_layer) + .with(otel_layer) + .try_init(); + } else { + let _ = tracing_subscriber::registry().with(file_layer).try_init(); + }; run_ratatui_app(cli, config, active_profile, should_show_trust_screen) .await diff --git a/docs/config.md b/docs/config.md index ba204ee00a..e0d4e66c7c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -435,6 +435,117 @@ set = { PATH = "/usr/bin", MY_FLAG = "1" } Currently, `CODEX_SANDBOX_NETWORK_DISABLED=1` is also added to the environment, assuming network is disabled. This is not configurable. +## otel + +Codex can emit [OpenTelemetry](https://opentelemetry.io/) **log events** that +describe each run: outbound API requests, streamed responses, user input, +tool-approval decisions, and the result of every tool invocation. Export is +**disabled by default** so local runs remain self-contained. Opt in by adding an +`[otel]` table and choosing an exporter. + +```toml +[otel] +environment = "staging" # defaults to "dev" +exporter = "none" # defaults to "none"; set to otlp-http or otlp-grpc to send events +log_user_prompt = false # defaults to false; redact prompt text unless explicitly enabled +``` + +Codex tags every exported event with `service.name = $ORIGINATOR` (the same +value sent in the `originator` header, `codex_cli_rs` by default), the CLI +version, and an `env` attribute so downstream collectors can distinguish +dev/staging/prod traffic. Only telemetry produced inside the `codex_otel` +crate—the events listed below—is forwarded to the exporter. + +### Event catalog + +Every event shares a common set of metadata fields: `event.timestamp`, +`conversation.id`, `app.version`, `auth_mode` (when available), +`user.account_id` (when available), `terminal.type`, `model`, and `slug`. + +With OTEL enabled Codex emits the following event types (in addition to the +metadata above): + +- `codex.conversation_starts` + - `provider_name` + - `reasoning_effort` (optional) + - `reasoning_summary` + - `context_window` (optional) + - `max_output_tokens` (optional) + - `auto_compact_token_limit` (optional) + - `approval_policy` + - `sandbox_policy` + - `mcp_servers` (comma-separated list) + - `active_profile` (optional) +- `codex.api_request` + - `attempt` + - `duration_ms` + - `http.response.status_code` (optional) + - `error.message` (failures) +- `codex.sse_event` + - `event.kind` + - `duration_ms` + - `error.message` (failures) + - `input_token_count` (responses only) + - `output_token_count` (responses only) + - `cached_token_count` (responses only, optional) + - `reasoning_token_count` (responses only, optional) + - `tool_token_count` (responses only) +- `codex.user_prompt` + - `prompt_length` + - `prompt` (redacted unless `log_user_prompt = true`) +- `codex.tool_decision` + - `tool_name` + - `call_id` + - `decision` (`approved`, `approved_for_session`, `denied`, or `abort`) + - `source` (`config` or `user`) +- `codex.tool_result` + - `tool_name` + - `call_id` (optional) + - `arguments` (optional) + - `duration_ms` (execution time for the tool) + - `success` (`"true"` or `"false"`) + - `output` + +These event shapes may change as we iterate. + +### Choosing an exporter + +Set `otel.exporter` to control where events go: + +- `none` – leaves instrumentation active but skips exporting. This is the + default. +- `otlp-http` – posts OTLP log records to an OTLP/HTTP collector. Specify the + endpoint, protocol, and headers your collector expects: + + ```toml + [otel] + exporter = { otlp-http = { + endpoint = "https://otel.example.com/v1/logs", + protocol = "binary", + headers = { "x-otlp-api-key" = "${OTLP_TOKEN}" } + }} + ``` + +- `otlp-grpc` – streams OTLP log records over gRPC. Provide the endpoint and any + metadata headers: + + ```toml + [otel] + exporter = { otlp-grpc = { + endpoint = "https://otel.example.com:4317", + headers = { "x-otlp-meta" = "abc123" } + }} + ``` + +If the exporter is `none` nothing is written anywhere; otherwise you must run or point to your +own collector. All exporters run on a background batch worker that is flushed on +shutdown. + +If you build Codex from source the OTEL crate is still behind an `otel` feature +flag; the official prebuilt binaries ship with the feature enabled. When the +feature is disabled the telemetry hooks become no-ops so the CLI continues to +function without the extra dependencies. + ## notify Specify a program that will be executed to get notified about events generated by Codex. Note that the program will receive the notification argument as a string of JSON, e.g.: From 197f45a3be86ffa12d17dc285e436c206212905f Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 29 Sep 2025 12:19:09 -0700 Subject: [PATCH 03/23] [mcp-server] Expose fuzzy file search in MCP (#2677) ## Summary Expose a simple fuzzy file search implementation for mcp clients to work with ## Testing - [x] Tested locally --- codex-rs/Cargo.lock | 1 + codex-rs/file-search/src/lib.rs | 23 +++- codex-rs/mcp-server/Cargo.toml | 1 + .../mcp-server/src/codex_message_processor.rs | 50 +++++++++ codex-rs/mcp-server/src/fuzzy_file_search.rs | 84 ++++++++++++++ codex-rs/mcp-server/src/lib.rs | 1 + .../mcp-server/tests/common/mcp_process.rs | 17 +++ .../tests/suite/fuzzy_file_search.rs | 104 ++++++++++++++++++ codex-rs/mcp-server/tests/suite/mod.rs | 1 + codex-rs/protocol-ts/src/lib.rs | 3 + codex-rs/protocol/src/mcp_protocol.rs | 31 ++++++ 11 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 codex-rs/mcp-server/src/fuzzy_file_search.rs create mode 100644 codex-rs/mcp-server/tests/suite/fuzzy_file_search.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 96bb30f73a..b223a2440b 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -916,6 +916,7 @@ dependencies = [ "codex-arg0", "codex-common", "codex-core", + "codex-file-search", "codex-login", "codex-protocol", "core_test_support", diff --git a/codex-rs/file-search/src/lib.rs b/codex-rs/file-search/src/lib.rs index 3238c99f24..82b8fcb695 100644 --- a/codex-rs/file-search/src/lib.rs +++ b/codex-rs/file-search/src/lib.rs @@ -287,10 +287,27 @@ pub fn run( /// Sort matches in-place by descending score, then ascending path. fn sort_matches(matches: &mut [(u32, String)]) { - matches.sort_by(|a, b| match b.0.cmp(&a.0) { - std::cmp::Ordering::Equal => a.1.cmp(&b.1), + matches.sort_by(cmp_by_score_desc_then_path_asc::<(u32, String), _, _>( + |t| t.0, + |t| t.1.as_str(), + )); +} + +/// Returns a comparator closure suitable for `slice.sort_by(...)` that orders +/// items by descending score and then ascending path using the provided accessors. +pub fn cmp_by_score_desc_then_path_asc( + score_of: FScore, + path_of: FPath, +) -> impl FnMut(&T, &T) -> std::cmp::Ordering +where + FScore: Fn(&T) -> u32, + FPath: Fn(&T) -> &str, +{ + use std::cmp::Ordering; + move |a, b| match score_of(b).cmp(&score_of(a)) { + Ordering::Equal => path_of(a).cmp(path_of(b)), other => other, - }); + } } /// Maintains the `max_count` best matches for a given pattern. diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index e40dd15df7..9a5fd947d8 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -19,6 +19,7 @@ anyhow = { workspace = true } codex-arg0 = { workspace = true } codex-common = { workspace = true, features = ["cli"] } codex-core = { workspace = true } +codex-file-search = { workspace = true } codex-login = { workspace = true } codex-protocol = { workspace = true } mcp-types = { workspace = true } diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs index eec3a62a2c..53830431fd 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/mcp-server/src/codex_message_processor.rs @@ -1,5 +1,6 @@ use crate::error_code::INTERNAL_ERROR_CODE; use crate::error_code::INVALID_REQUEST_ERROR_CODE; +use crate::fuzzy_file_search::run_fuzzy_file_search; use crate::json_to_toml::json_to_toml; use crate::outgoing_message::OutgoingMessageSender; use crate::outgoing_message::OutgoingNotification; @@ -52,6 +53,8 @@ use codex_protocol::mcp_protocol::ExecArbitraryCommandResponse; use codex_protocol::mcp_protocol::ExecCommandApprovalParams; use codex_protocol::mcp_protocol::ExecCommandApprovalResponse; use codex_protocol::mcp_protocol::ExecOneOffCommandParams; +use codex_protocol::mcp_protocol::FuzzyFileSearchParams; +use codex_protocol::mcp_protocol::FuzzyFileSearchResponse; use codex_protocol::mcp_protocol::GetUserAgentResponse; use codex_protocol::mcp_protocol::GetUserSavedConfigResponse; use codex_protocol::mcp_protocol::GitDiffToRemoteResponse; @@ -88,6 +91,8 @@ use std::collections::HashMap; use std::ffi::OsStr; use std::path::PathBuf; use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; use std::time::Duration; use tokio::select; use tokio::sync::Mutex; @@ -122,6 +127,7 @@ pub(crate) struct CodexMessageProcessor { active_login: Arc>>, // Queue of pending interrupt requests per conversation. We reply when TurnAborted arrives. pending_interrupts: Arc>>>, + pending_fuzzy_searches: Arc>>>, } impl CodexMessageProcessor { @@ -141,6 +147,7 @@ impl CodexMessageProcessor { conversation_listeners: HashMap::new(), active_login: Arc::new(Mutex::new(None)), pending_interrupts: Arc::new(Mutex::new(HashMap::new())), + pending_fuzzy_searches: Arc::new(Mutex::new(HashMap::new())), } } @@ -206,6 +213,9 @@ impl CodexMessageProcessor { ClientRequest::UserInfo { request_id } => { self.get_user_info(request_id).await; } + ClientRequest::FuzzyFileSearch { request_id, params } => { + self.fuzzy_file_search(request_id, params).await; + } ClientRequest::ExecOneOffCommand { request_id, params } => { self.exec_one_off_command(request_id, params).await; } @@ -1167,6 +1177,46 @@ impl CodexMessageProcessor { } } } + + async fn fuzzy_file_search(&mut self, request_id: RequestId, params: FuzzyFileSearchParams) { + let FuzzyFileSearchParams { + query, + roots, + cancellation_token, + } = params; + + let cancel_flag = match cancellation_token.clone() { + Some(token) => { + let mut pending_fuzzy_searches = self.pending_fuzzy_searches.lock().await; + // if a cancellation_token is provided and a pending_request exists for + // that token, cancel it + if let Some(existing) = pending_fuzzy_searches.get(&token) { + existing.store(true, Ordering::Relaxed); + } + let flag = Arc::new(AtomicBool::new(false)); + pending_fuzzy_searches.insert(token.clone(), flag.clone()); + flag + } + None => Arc::new(AtomicBool::new(false)), + }; + + let results = match query.as_str() { + "" => vec![], + _ => run_fuzzy_file_search(query, roots, cancel_flag.clone()).await, + }; + + if let Some(token) = cancellation_token { + let mut pending_fuzzy_searches = self.pending_fuzzy_searches.lock().await; + if let Some(current_flag) = pending_fuzzy_searches.get(&token) + && Arc::ptr_eq(current_flag, &cancel_flag) + { + pending_fuzzy_searches.remove(&token); + } + } + + let response = FuzzyFileSearchResponse { files: results }; + self.outgoing.send_response(request_id, response).await; + } } async fn apply_bespoke_event_handling( diff --git a/codex-rs/mcp-server/src/fuzzy_file_search.rs b/codex-rs/mcp-server/src/fuzzy_file_search.rs new file mode 100644 index 0000000000..eef760df26 --- /dev/null +++ b/codex-rs/mcp-server/src/fuzzy_file_search.rs @@ -0,0 +1,84 @@ +use std::num::NonZero; +use std::num::NonZeroUsize; +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; + +use codex_file_search as file_search; +use codex_protocol::mcp_protocol::FuzzyFileSearchResult; +use tokio::task::JoinSet; +use tracing::warn; + +const LIMIT_PER_ROOT: usize = 50; +const MAX_THREADS: usize = 12; +const COMPUTE_INDICES: bool = true; + +pub(crate) async fn run_fuzzy_file_search( + query: String, + roots: Vec, + cancellation_flag: Arc, +) -> Vec { + #[expect(clippy::expect_used)] + let limit_per_root = + NonZero::new(LIMIT_PER_ROOT).expect("LIMIT_PER_ROOT should be a valid non-zero usize"); + + let cores = std::thread::available_parallelism() + .map(std::num::NonZero::get) + .unwrap_or(1); + let threads = cores.min(MAX_THREADS); + let threads_per_root = (threads / roots.len()).max(1); + let threads = NonZero::new(threads_per_root).unwrap_or(NonZeroUsize::MIN); + + let mut files: Vec = Vec::new(); + let mut join_set = JoinSet::new(); + + for root in roots { + let search_dir = PathBuf::from(&root); + let query = query.clone(); + let cancel_flag = cancellation_flag.clone(); + join_set.spawn_blocking(move || { + match file_search::run( + query.as_str(), + limit_per_root, + &search_dir, + Vec::new(), + threads, + cancel_flag, + COMPUTE_INDICES, + ) { + Ok(res) => Ok((root, res)), + Err(err) => Err((root, err)), + } + }); + } + + while let Some(res) = join_set.join_next().await { + match res { + Ok(Ok((root, res))) => { + for m in res.matches { + let result = FuzzyFileSearchResult { + root: root.clone(), + path: m.path, + score: m.score, + indices: m.indices, + }; + files.push(result); + } + } + Ok(Err((root, err))) => { + warn!("fuzzy-file-search in dir '{root}' failed: {err}"); + } + Err(err) => { + warn!("fuzzy-file-search join_next failed: {err}"); + } + } + } + + files.sort_by(file_search::cmp_by_score_desc_then_path_asc::< + FuzzyFileSearchResult, + _, + _, + >(|f| f.score, |f| f.path.as_str())); + + files +} diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs index b1397846ca..830bf49247 100644 --- a/codex-rs/mcp-server/src/lib.rs +++ b/codex-rs/mcp-server/src/lib.rs @@ -25,6 +25,7 @@ mod codex_tool_config; mod codex_tool_runner; mod error_code; mod exec_approval; +mod fuzzy_file_search; mod json_to_toml; pub(crate) mod message_processor; mod outgoing_message; diff --git a/codex-rs/mcp-server/tests/common/mcp_process.rs b/codex-rs/mcp-server/tests/common/mcp_process.rs index a30e9817f2..f89f72f5a4 100644 --- a/codex-rs/mcp-server/tests/common/mcp_process.rs +++ b/codex-rs/mcp-server/tests/common/mcp_process.rs @@ -357,6 +357,23 @@ impl McpProcess { self.send_request("logoutChatGpt", None).await } + /// Send a `fuzzyFileSearch` JSON-RPC request. + pub async fn send_fuzzy_file_search_request( + &mut self, + query: &str, + roots: Vec, + cancellation_token: Option, + ) -> anyhow::Result { + let mut params = serde_json::json!({ + "query": query, + "roots": roots, + }); + if let Some(token) = cancellation_token { + params["cancellationToken"] = serde_json::json!(token); + } + self.send_request("fuzzyFileSearch", Some(params)).await + } + async fn send_request( &mut self, method: &str, diff --git a/codex-rs/mcp-server/tests/suite/fuzzy_file_search.rs b/codex-rs/mcp-server/tests/suite/fuzzy_file_search.rs new file mode 100644 index 0000000000..e4aa0add9c --- /dev/null +++ b/codex-rs/mcp-server/tests/suite/fuzzy_file_search.rs @@ -0,0 +1,104 @@ +use mcp_test_support::McpProcess; +use mcp_types::JSONRPCResponse; +use mcp_types::RequestId; +use pretty_assertions::assert_eq; +use serde_json::json; +use tempfile::TempDir; +use tokio::time::timeout; + +const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_fuzzy_file_search_sorts_and_includes_indices() { + // Prepare a temporary Codex home and a separate root with test files. + let codex_home = TempDir::new().expect("create temp codex home"); + let root = TempDir::new().expect("create temp search root"); + + // Create files designed to have deterministic ordering for query "abc". + std::fs::write(root.path().join("abc"), "x").expect("write file abc"); + std::fs::write(root.path().join("abcde"), "x").expect("write file abcx"); + std::fs::write(root.path().join("abexy"), "x").expect("write file abcx"); + std::fs::write(root.path().join("zzz.txt"), "x").expect("write file zzz"); + + // Start MCP server and initialize. + let mut mcp = McpProcess::new(codex_home.path()).await.expect("spawn mcp"); + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()) + .await + .expect("init timeout") + .expect("init failed"); + + let root_path = root.path().to_string_lossy().to_string(); + // Send fuzzyFileSearch request. + let request_id = mcp + .send_fuzzy_file_search_request("abe", vec![root_path.clone()], None) + .await + .expect("send fuzzyFileSearch"); + + // Read response and verify shape and ordering. + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await + .expect("fuzzyFileSearch timeout") + .expect("fuzzyFileSearch resp"); + + let value = resp.result; + assert_eq!( + value, + json!({ + "files": [ + { "root": root_path.clone(), "path": "abexy", "score": 88, "indices": [0, 1, 2] }, + { "root": root_path.clone(), "path": "abcde", "score": 74, "indices": [0, 1, 4] }, + ] + }) + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_fuzzy_file_search_accepts_cancellation_token() { + let codex_home = TempDir::new().expect("create temp codex home"); + let root = TempDir::new().expect("create temp search root"); + + std::fs::write(root.path().join("alpha.txt"), "contents").expect("write alpha"); + + let mut mcp = McpProcess::new(codex_home.path()).await.expect("spawn mcp"); + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()) + .await + .expect("init timeout") + .expect("init failed"); + + let root_path = root.path().to_string_lossy().to_string(); + let request_id = mcp + .send_fuzzy_file_search_request("alp", vec![root_path.clone()], None) + .await + .expect("send fuzzyFileSearch"); + + let request_id_2 = mcp + .send_fuzzy_file_search_request( + "alp", + vec![root_path.clone()], + Some(request_id.to_string()), + ) + .await + .expect("send fuzzyFileSearch"); + + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id_2)), + ) + .await + .expect("fuzzyFileSearch timeout") + .expect("fuzzyFileSearch resp"); + + let files = resp + .result + .get("files") + .and_then(|value| value.as_array()) + .cloned() + .expect("files array"); + + assert_eq!(files.len(), 1); + assert_eq!(files[0]["root"], root_path); + assert_eq!(files[0]["path"], "alpha.txt"); +} diff --git a/codex-rs/mcp-server/tests/suite/mod.rs b/codex-rs/mcp-server/tests/suite/mod.rs index 97e5370908..4a3a91206f 100644 --- a/codex-rs/mcp-server/tests/suite/mod.rs +++ b/codex-rs/mcp-server/tests/suite/mod.rs @@ -5,6 +5,7 @@ mod codex_message_processor_flow; mod codex_tool; mod config; mod create_conversation; +mod fuzzy_file_search; mod interrupt; mod list_resume; mod login; diff --git a/codex-rs/protocol-ts/src/lib.rs b/codex-rs/protocol-ts/src/lib.rs index 3aec3d892f..aaf820965d 100644 --- a/codex-rs/protocol-ts/src/lib.rs +++ b/codex-rs/protocol-ts/src/lib.rs @@ -39,6 +39,9 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { codex_protocol::mcp_protocol::GetAuthStatusResponse::export_all_to(out_dir)?; codex_protocol::mcp_protocol::ApplyPatchApprovalResponse::export_all_to(out_dir)?; codex_protocol::mcp_protocol::ExecCommandApprovalResponse::export_all_to(out_dir)?; + codex_protocol::mcp_protocol::FuzzyFileSearchParams::export_all_to(out_dir)?; + codex_protocol::mcp_protocol::FuzzyFileSearchResult::export_all_to(out_dir)?; + codex_protocol::mcp_protocol::FuzzyFileSearchResponse::export_all_to(out_dir)?; codex_protocol::mcp_protocol::GetUserSavedConfigResponse::export_all_to(out_dir)?; codex_protocol::mcp_protocol::SetDefaultModelResponse::export_all_to(out_dir)?; codex_protocol::mcp_protocol::GetUserAgentResponse::export_all_to(out_dir)?; diff --git a/codex-rs/protocol/src/mcp_protocol.rs b/codex-rs/protocol/src/mcp_protocol.rs index a9f808b16e..ee6a56a86f 100644 --- a/codex-rs/protocol/src/mcp_protocol.rs +++ b/codex-rs/protocol/src/mcp_protocol.rs @@ -184,6 +184,11 @@ pub enum ClientRequest { #[serde(rename = "id")] request_id: RequestId, }, + FuzzyFileSearch { + #[serde(rename = "id")] + request_id: RequestId, + params: FuzzyFileSearchParams, + }, /// Execute a command (argv vector) under the server's sandbox. ExecOneOffCommand { #[serde(rename = "id")] @@ -662,6 +667,32 @@ pub struct ApplyPatchApprovalResponse { pub decision: ReviewDecision, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct FuzzyFileSearchParams { + pub query: String, + pub roots: Vec, + // if provided, will cancel any previous request that used the same value + #[serde(skip_serializing_if = "Option::is_none")] + pub cancellation_token: Option, +} + +/// Superset of [`codex_file_search::FileMatch`] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +pub struct FuzzyFileSearchResult { + pub root: String, + pub path: String, + pub score: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub indices: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +pub struct FuzzyFileSearchResponse { + pub files: Vec, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct LoginChatGptCompleteNotification { From 83a4d4d8ed43dc8c036516757a1d680ea5c6fd8b Mon Sep 17 00:00:00 2001 From: dedrisian-oai Date: Mon, 29 Sep 2025 13:06:08 -0700 Subject: [PATCH 04/23] Parse out frontmatter for custom prompts (#4456) [Cherry picked from https://github.com/openai/codex/pull/3565] Removes the frontmatter description/args from custom prompt files and only includes body. --- codex-rs/core/src/custom_prompts.rs | 101 +++++++++++++++++- codex-rs/protocol/src/custom_prompts.rs | 2 + codex-rs/tui/src/bottom_pane/chat_composer.rs | 2 + codex-rs/tui/src/bottom_pane/command_popup.rs | 6 ++ 4 files changed, 110 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/custom_prompts.rs b/codex-rs/core/src/custom_prompts.rs index 357abef55b..d92c683902 100644 --- a/codex-rs/core/src/custom_prompts.rs +++ b/codex-rs/core/src/custom_prompts.rs @@ -63,16 +63,88 @@ pub async fn discover_prompts_in_excluding( Ok(s) => s, Err(_) => continue, }; + let (description, argument_hint, body) = parse_frontmatter(&content); out.push(CustomPrompt { name, path, - content, + content: body, + description, + argument_hint, }); } out.sort_by(|a, b| a.name.cmp(&b.name)); out } +/// Parse optional YAML-like frontmatter at the beginning of `content`. +/// Supported keys: +/// - `description`: short description shown in the slash popup +/// - `argument-hint` or `argument_hint`: brief hint string shown after the description +/// Returns (description, argument_hint, body_without_frontmatter). +fn parse_frontmatter(content: &str) -> (Option, Option, String) { + let mut segments = content.split_inclusive('\n'); + let Some(first_segment) = segments.next() else { + return (None, None, String::new()); + }; + let first_line = first_segment.trim_end_matches(['\r', '\n']); + if first_line.trim() != "---" { + return (None, None, content.to_string()); + } + + let mut desc: Option = None; + let mut hint: Option = None; + let mut frontmatter_closed = false; + let mut consumed = first_segment.len(); + + for segment in segments { + let line = segment.trim_end_matches(['\r', '\n']); + let trimmed = line.trim(); + + if trimmed == "---" { + frontmatter_closed = true; + consumed += segment.len(); + break; + } + + if trimmed.is_empty() || trimmed.starts_with('#') { + consumed += segment.len(); + continue; + } + + if let Some((k, v)) = trimmed.split_once(':') { + let key = k.trim().to_ascii_lowercase(); + let mut val = v.trim().to_string(); + if val.len() >= 2 { + let bytes = val.as_bytes(); + let first = bytes[0]; + let last = bytes[bytes.len() - 1]; + if (first == b'\"' && last == b'\"') || (first == b'\'' && last == b'\'') { + val = val[1..val.len().saturating_sub(1)].to_string(); + } + } + match key.as_str() { + "description" => desc = Some(val), + "argument-hint" | "argument_hint" => hint = Some(val), + _ => {} + } + } + + consumed += segment.len(); + } + + if !frontmatter_closed { + // Unterminated frontmatter: treat input as-is. + return (None, None, content.to_string()); + } + + let body = if consumed >= content.len() { + String::new() + } else { + content[consumed..].to_string() + }; + (desc, hint, body) +} + #[cfg(test)] mod tests { use super::*; @@ -124,4 +196,31 @@ mod tests { let names: Vec = found.into_iter().map(|e| e.name).collect(); assert_eq!(names, vec!["good"]); } + + #[tokio::test] + async fn parses_frontmatter_and_strips_from_body() { + let tmp = tempdir().expect("create TempDir"); + let dir = tmp.path(); + let file = dir.join("withmeta.md"); + let text = "---\nname: ignored\ndescription: \"Quick review command\"\nargument-hint: \"[file] [priority]\"\n---\nActual body with $1 and $ARGUMENTS"; + fs::write(&file, text).unwrap(); + + let found = discover_prompts_in(dir).await; + assert_eq!(found.len(), 1); + let p = &found[0]; + assert_eq!(p.name, "withmeta"); + assert_eq!(p.description.as_deref(), Some("Quick review command")); + assert_eq!(p.argument_hint.as_deref(), Some("[file] [priority]")); + // Body should not include the frontmatter delimiters. + assert_eq!(p.content, "Actual body with $1 and $ARGUMENTS"); + } + + #[test] + fn parse_frontmatter_preserves_body_newlines() { + let content = "---\r\ndescription: \"Line endings\"\r\nargument_hint: \"[arg]\"\r\n---\r\nFirst line\r\nSecond line\r\n"; + let (desc, hint, body) = parse_frontmatter(content); + assert_eq!(desc.as_deref(), Some("Line endings")); + assert_eq!(hint.as_deref(), Some("[arg]")); + assert_eq!(body, "First line\r\nSecond line\r\n"); + } } diff --git a/codex-rs/protocol/src/custom_prompts.rs b/codex-rs/protocol/src/custom_prompts.rs index be402051b5..7bceb1390f 100644 --- a/codex-rs/protocol/src/custom_prompts.rs +++ b/codex-rs/protocol/src/custom_prompts.rs @@ -8,4 +8,6 @@ pub struct CustomPrompt { pub name: String, pub path: PathBuf, pub content: String, + pub description: Option, + pub argument_hint: Option, } diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index b3c1eea6d9..f9e9dd769d 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2258,6 +2258,8 @@ mod tests { name: "my-prompt".to_string(), path: "/tmp/my-prompt.md".to_string().into(), content: prompt_text.to_string(), + description: None, + argument_hint: None, }]); type_chars_humanlike( diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 32216f0776..f002acfe1e 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -276,11 +276,15 @@ mod tests { name: "foo".to_string(), path: "/tmp/foo.md".to_string().into(), content: "hello from foo".to_string(), + description: None, + argument_hint: None, }, CustomPrompt { name: "bar".to_string(), path: "/tmp/bar.md".to_string().into(), content: "hello from bar".to_string(), + description: None, + argument_hint: None, }, ]; let popup = CommandPopup::new(prompts); @@ -303,6 +307,8 @@ mod tests { name: "init".to_string(), path: "/tmp/init.md".to_string().into(), content: "should be ignored".to_string(), + description: None, + argument_hint: None, }]); let items = popup.filtered_items(); let has_collision_prompt = items.into_iter().any(|it| match it { From adbc38a97871dcb472cccf5a349e0c3b0f11db13 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 29 Sep 2025 13:27:13 -0700 Subject: [PATCH 05/23] TypeScript SDK scaffold (#4455) --- .codespellrc | 2 +- .github/workflows/codespell.yml | 1 - pnpm-lock.yaml | 4436 ++++++++++++++++++++++++++++ pnpm-workspace.yaml | 1 + sdk/typescript/.eslintrc.json | 40 + sdk/typescript/.prettierignore | 3 + sdk/typescript/.prettierrc | 5 + sdk/typescript/package.json | 56 + sdk/typescript/src/index.ts | 4 + sdk/typescript/tests/index.test.ts | 9 + sdk/typescript/tsconfig.json | 22 + sdk/typescript/tsup.config.ts | 12 + sdk/typescript/vitest.config.ts | 11 + 13 files changed, 4600 insertions(+), 2 deletions(-) create mode 100644 sdk/typescript/.eslintrc.json create mode 100644 sdk/typescript/.prettierignore create mode 100644 sdk/typescript/.prettierrc create mode 100644 sdk/typescript/package.json create mode 100644 sdk/typescript/src/index.ts create mode 100644 sdk/typescript/tests/index.test.ts create mode 100644 sdk/typescript/tsconfig.json create mode 100644 sdk/typescript/tsup.config.ts create mode 100644 sdk/typescript/vitest.config.ts diff --git a/.codespellrc b/.codespellrc index eefde42a4e..da831d8957 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,6 +1,6 @@ [codespell] # Ref: https://github.com/codespell-project/codespell#using-a-config-file -skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl +skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt check-hidden = true ignore-regex = ^\s*"image/\S+": ".*|\b(afterAll)\b ignore-words-list = ratatui,ser diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 3d49986791..ad16769471 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -25,4 +25,3 @@ jobs: uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2.1 with: ignore_words_file: .codespellignore - skip: frame*.txt diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 732e02b0bd..28bcb48ede 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,13 +17,4449 @@ importers: specifier: ^3.5.3 version: 3.5.3 + sdk/typescript: + devDependencies: + '@types/node': + specifier: ^20.19.18 + version: 20.19.18 + '@typescript-eslint/eslint-plugin': + specifier: ^7.18.0 + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^7.18.0 + version: 7.18.0(eslint@9.36.0)(typescript@5.9.2) + '@vitest/coverage-v8': + specifier: ^1.6.1 + version: 1.6.1(vitest@1.6.1(@types/node@20.19.18)) + eslint: + specifier: ^9.36.0 + version: 9.36.0 + eslint-config-prettier: + specifier: ^9.1.2 + version: 9.1.2(eslint@9.36.0) + eslint-import-resolver-typescript: + specifier: ^3.10.1 + version: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0) + eslint-plugin-import: + specifier: ^2.32.0 + version: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + tsup: + specifier: ^8.5.0 + version: 8.5.0(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1) + typescript: + specifier: ^5.9.2 + version: 5.9.2 + vitest: + specifier: ^1.6.1 + version: 1.6.1(@types/node@20.19.18) + packages: + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@emnapi/core@1.5.0': + resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.36.0': + resolution: {integrity: sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/rollup-android-arm-eabi@4.52.3': + resolution: {integrity: sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.3': + resolution: {integrity: sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.3': + resolution: {integrity: sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.3': + resolution: {integrity: sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.3': + resolution: {integrity: sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.3': + resolution: {integrity: sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.3': + resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.3': + resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.3': + resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.3': + resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.3': + resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.3': + resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.3': + resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.3': + resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.3': + resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.3': + resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.3': + resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.3': + resolution: {integrity: sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.3': + resolution: {integrity: sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.3': + resolution: {integrity: sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.3': + resolution: {integrity: sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@20.19.18': + resolution: {integrity: sha512-KeYVbfnbsBCyKG8e3gmUqAfyZNcoj/qpEbHRkQkfZdKOBrU7QQ+BsTdfqLSWX9/m1ytYreMhpKvp+EZi3UFYAg==} + + '@typescript-eslint/eslint-plugin@7.18.0': + resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.18.0': + resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@7.18.0': + resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/type-utils@7.18.0': + resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@7.18.0': + resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/typescript-estree@7.18.0': + resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@7.18.0': + resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/visitor-keys@7.18.0': + resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + '@vitest/coverage-v8@1.6.1': + resolution: {integrity: sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==} + peerDependencies: + vitest: 1.6.1 + + '@vitest/expect@1.6.1': + resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} + + '@vitest/runner@1.6.1': + resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} + + '@vitest/snapshot@1.6.1': + resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} + + '@vitest/spy@1.6.1': + resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} + + '@vitest/utils@1.6.1': + resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@9.1.2: + resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.36.0: + resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.0: + resolution: {integrity: sha512-xPypGGincdfyl/AiSGa7GjXLkvld9V7GjZlowup9SHIJnQnHLFiLODCd/DqKOp0PBagbHJ68r1KJI9Mut7m4sA==} + engines: {node: '>= 0.4'} + + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.1: + resolution: {integrity: sha512-Gn8BWUdrTzf9XUJAvqIYP7QnSC3mKs8QjQdGdJ7HmBemzZo14wj/OVmmAwgxDX/7WhFEjboybL4VhXGIQYPlOA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.3: + resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + prettier@3.5.3: resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} hasBin: true + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.52.3: + resolution: {integrity: sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + engines: {node: '>=14.0.0'} + + tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsup@8.5.0: + resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite-node@1.6.1: + resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.20: + resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@1.6.1: + resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.1 + '@vitest/ui': 1.6.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + snapshots: + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@0.2.3': {} + + '@emnapi/core@1.5.0': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0)': + dependencies: + eslint: 9.36.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.36.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/rollup-android-arm-eabi@4.52.3': + optional: true + + '@rollup/rollup-android-arm64@4.52.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.3': + optional: true + + '@rollup/rollup-darwin-x64@4.52.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.3': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@sinclair/typebox@0.27.8': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@20.19.18': + dependencies: + undici-types: 6.21.0 + + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 7.18.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/type-utils': 7.18.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/utils': 7.18.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 7.18.0 + eslint: 9.36.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.3(typescript@5.9.2) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2)': + dependencies: + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + eslint: 9.36.0 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + + '@typescript-eslint/type-utils@7.18.0(eslint@9.36.0)(typescript@5.9.2)': + dependencies: + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) + '@typescript-eslint/utils': 7.18.0(eslint@9.36.0)(typescript@5.9.2) + debug: 4.4.3 + eslint: 9.36.0 + ts-api-utils: 1.4.3(typescript@5.9.2) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@7.18.0': {} + + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.2)': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 1.4.3(typescript@5.9.2) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.18.0(eslint@9.36.0)(typescript@5.9.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) + eslint: 9.36.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + eslint-visitor-keys: 3.4.3 + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + '@vitest/coverage-v8@1.6.1(vitest@1.6.1(@types/node@20.19.18))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.19 + magicast: 0.3.5 + picocolors: 1.1.1 + std-env: 3.9.0 + strip-literal: 2.1.1 + test-exclude: 6.0.0 + vitest: 1.6.1(@types/node@20.19.18) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@1.6.1': + dependencies: + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + chai: 4.5.0 + + '@vitest/runner@1.6.1': + dependencies: + '@vitest/utils': 1.6.1 + p-limit: 5.0.0 + pathe: 1.1.2 + + '@vitest/snapshot@1.6.1': + dependencies: + magic-string: 0.30.19 + pathe: 1.1.2 + pretty-format: 29.7.0 + + '@vitest/spy@1.6.1': + dependencies: + tinyspy: 2.2.1 + + '@vitest/utils@1.6.1': + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array-union@2.1.0: {} + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@1.1.0: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + bundle-require@5.1.0(esbuild@0.25.10): + dependencies: + esbuild: 0.25.10 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + chai@4.5.0: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.1.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@4.1.4: + dependencies: + type-detect: 4.1.0 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + diff-sequences@29.6.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@9.1.2(eslint@9.36.0): + dependencies: + eslint: 9.36.0 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.36.0 + get-tsconfig: 4.10.1 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 7.18.0(eslint@9.36.0)(typescript@5.9.2) + eslint: 9.36.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.36.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 7.7.2 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 7.18.0(eslint@9.36.0)(typescript@5.9.2) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.36.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.36.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.19 + mlly: 1.8.0 + rollup: 4.52.3 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.0: {} + + get-func-name@2.0.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@8.0.1: {} + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-escaper@2.0.2: {} + + human-signals@5.0.0: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.1: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.0 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@3.0.0: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + joycon@3.1.1: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + local-pkg@0.5.1: + dependencies: + mlly: 1.8.0 + pkg-types: 1.3.1 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash.sortby@4.7.0: {} + + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + + lru-cache@10.4.3: {} + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.2 + + math-intrinsics@1.1.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-fn@4.0.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + napi-postinstall@0.3.3: {} + + natural-compare@1.4.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@5.0.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-type@4.0.0: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@1.1.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + possible-typed-array-names@1.1.0: {} + + postcss-load-config@6.0.1(postcss@8.5.6)(yaml@2.8.1): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.6 + yaml: 2.8.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + prettier@3.5.3: {} + + prettier@3.6.2: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-is@18.3.1: {} + + readdirp@4.1.2: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.52.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.3 + '@rollup/rollup-android-arm64': 4.52.3 + '@rollup/rollup-darwin-arm64': 4.52.3 + '@rollup/rollup-darwin-x64': 4.52.3 + '@rollup/rollup-freebsd-arm64': 4.52.3 + '@rollup/rollup-freebsd-x64': 4.52.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.3 + '@rollup/rollup-linux-arm-musleabihf': 4.52.3 + '@rollup/rollup-linux-arm64-gnu': 4.52.3 + '@rollup/rollup-linux-arm64-musl': 4.52.3 + '@rollup/rollup-linux-loong64-gnu': 4.52.3 + '@rollup/rollup-linux-ppc64-gnu': 4.52.3 + '@rollup/rollup-linux-riscv64-gnu': 4.52.3 + '@rollup/rollup-linux-riscv64-musl': 4.52.3 + '@rollup/rollup-linux-s390x-gnu': 4.52.3 + '@rollup/rollup-linux-x64-gnu': 4.52.3 + '@rollup/rollup-linux-x64-musl': 4.52.3 + '@rollup/rollup-openharmony-arm64': 4.52.3 + '@rollup/rollup-win32-arm64-msvc': 4.52.3 + '@rollup/rollup-win32-ia32-msvc': 4.52.3 + '@rollup/rollup-win32-x64-gnu': 4.52.3 + '@rollup/rollup-win32-x64-msvc': 4.52.3 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + semver@7.7.2: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + + stable-hash@0.0.5: {} + + stackback@0.0.2: {} + + std-env@3.9.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@2.1.1: + dependencies: + js-tokens: 9.0.1 + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@0.8.4: {} + + tinyspy@2.2.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + ts-api-utils@1.4.3(typescript@5.9.2): + dependencies: + typescript: 5.9.2 + + ts-interface-checker@0.1.13: {} + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: + optional: true + + tsup@8.5.0(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.10) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.25.10 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.6)(yaml@2.8.1) + resolve-from: 5.0.0 + rollup: 4.52.3 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.6 + typescript: 5.9.2 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.1.0: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.2: {} + + ufo@1.6.1: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.3 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite-node@1.6.1(@types/node@20.19.18): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + pathe: 1.1.2 + picocolors: 1.1.1 + vite: 5.4.20(@types/node@20.19.18) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.20(@types/node@20.19.18): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.52.3 + optionalDependencies: + '@types/node': 20.19.18 + fsevents: 2.3.3 + + vitest@1.6.1(@types/node@20.19.18): + dependencies: + '@vitest/expect': 1.6.1 + '@vitest/runner': 1.6.1 + '@vitest/snapshot': 1.6.1 + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + acorn-walk: 8.3.4 + chai: 4.5.0 + debug: 4.4.3 + execa: 8.0.1 + local-pkg: 0.5.1 + magic-string: 0.30.19 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.9.0 + strip-literal: 2.1.1 + tinybench: 2.9.0 + tinypool: 0.8.4 + vite: 5.4.20(@types/node@20.19.18) + vite-node: 1.6.1(@types/node@20.19.18) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.18 + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + webidl-conversions@4.0.2: {} + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.1 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + yaml@2.8.1: + optional: true + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1c38470ff5..fef2aa1911 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - docs + - sdk/typescript ignoredBuiltDependencies: - esbuild diff --git a/sdk/typescript/.eslintrc.json b/sdk/typescript/.eslintrc.json new file mode 100644 index 0000000000..5a3226c2be --- /dev/null +++ b/sdk/typescript/.eslintrc.json @@ -0,0 +1,40 @@ +{ + "root": true, + "env": { + "node": true, + "es2022": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint", "import"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + "prettier" + ], + "settings": { + "import/resolver": { + "typescript": {} + } + }, + "rules": { + "import/order": [ + "error", + { + "newlines-between": "always", + "alphabetize": { "order": "asc", "caseInsensitive": true } + } + ], + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-expect-error": "allow-with-description" + } + ] + } +} diff --git a/sdk/typescript/.prettierignore b/sdk/typescript/.prettierignore new file mode 100644 index 0000000000..007ea8a7ce --- /dev/null +++ b/sdk/typescript/.prettierignore @@ -0,0 +1,3 @@ +dist +node_modules +coverage diff --git a/sdk/typescript/.prettierrc b/sdk/typescript/.prettierrc new file mode 100644 index 0000000000..5e2863a11f --- /dev/null +++ b/sdk/typescript/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 100, + "singleQuote": true, + "trailingComma": "all" +} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json new file mode 100644 index 0000000000..c74c34f9fe --- /dev/null +++ b/sdk/typescript/package.json @@ -0,0 +1,56 @@ +{ + "name": "@openai/codex-sdk", + "version": "0.0.0-dev", + "description": "TypeScript SDK for Codex APIs.", + "keywords": [ + "openai", + "codex", + "sdk", + "typescript", + "api" + ], + "license": "Apache-2.0", + "type": "module", + "engines": { + "node": ">=18" + }, + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "sideEffects": false, + "scripts": { + "clean": "rm -rf dist", + "build": "tsup", + "build:watch": "tsup --watch", + "lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\"", + "lint:fix": "pnpm run lint -- --fix", + "test": "vitest run", + "test:watch": "vitest watch", + "coverage": "vitest run --coverage", + "format": "prettier --check .", + "format:fix": "prettier --write .", + "prepare": "pnpm run build" + }, + "devDependencies": { + "@types/node": "^20.19.18", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@vitest/coverage-v8": "^1.6.1", + "eslint": "^9.36.0", + "eslint-config-prettier": "^9.1.2", + "eslint-import-resolver-typescript": "^3.10.1", + "eslint-plugin-import": "^2.32.0", + "prettier": "^3.6.2", + "tsup": "^8.5.0", + "typescript": "^5.9.2", + "vitest": "^1.6.1" + } +} diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts new file mode 100644 index 0000000000..40d4b9b2d0 --- /dev/null +++ b/sdk/typescript/src/index.ts @@ -0,0 +1,4 @@ +export class Codex { + constructor() { + } +} diff --git a/sdk/typescript/tests/index.test.ts b/sdk/typescript/tests/index.test.ts new file mode 100644 index 0000000000..c025118be2 --- /dev/null +++ b/sdk/typescript/tests/index.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from "vitest"; + +import { Codex } from "../src/index.js"; + +describe("Codex", () => { + it("exposes the placeholder API", () => { + const client = new Codex(); + }); +}); diff --git a/sdk/typescript/tsconfig.json b/sdk/typescript/tsconfig.json new file mode 100644 index 0000000000..a57827583c --- /dev/null +++ b/sdk/typescript/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "lib": ["ES2022"], + "types": ["node", "vitest"], + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist" + }, + "include": ["src", "tests", "vitest.config.ts", "tsup.config.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/sdk/typescript/tsup.config.ts b/sdk/typescript/tsup.config.ts new file mode 100644 index 0000000000..22bcd03938 --- /dev/null +++ b/sdk/typescript/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + sourcemap: true, + clean: true, + minify: false, + target: "node18", + shims: false, +}); diff --git a/sdk/typescript/vitest.config.ts b/sdk/typescript/vitest.config.ts new file mode 100644 index 0000000000..ad3095c2e9 --- /dev/null +++ b/sdk/typescript/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + coverage: { + provider: "v8", + reporter: ["text", "lcov"], + }, + }, +}); From 16057e76b0843afef5ac75c46f2989ab2a1347ff Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 29 Sep 2025 14:32:26 -0700 Subject: [PATCH 06/23] [Core]: add tail in the rollout data (#4461) This will help us show the conversation tail and last updated timestamp. --- codex-rs/core/src/rollout/list.rs | 101 +++++++++-- codex-rs/core/src/rollout/tests.rs | 268 +++++++++++++++++++++++++++++ codex-rs/tui/src/resume_picker.rs | 3 + 3 files changed, 362 insertions(+), 10 deletions(-) diff --git a/codex-rs/core/src/rollout/list.rs b/codex-rs/core/src/rollout/list.rs index 6f06709396..4ad7e73b89 100644 --- a/codex-rs/core/src/rollout/list.rs +++ b/codex-rs/core/src/rollout/list.rs @@ -36,13 +36,16 @@ pub struct ConversationsPage { pub struct ConversationItem { /// Absolute path to the rollout file. pub path: PathBuf, - /// First up to 5 JSONL records parsed as JSON (includes meta line). + /// First up to `HEAD_RECORD_LIMIT` JSONL records parsed as JSON (includes meta line). pub head: Vec, + /// Last up to `TAIL_RECORD_LIMIT` JSONL response records parsed as JSON. + pub tail: Vec, } /// Hard cap to bound worst‑case work per request. const MAX_SCAN_FILES: usize = 100; const HEAD_RECORD_LIMIT: usize = 10; +const TAIL_RECORD_LIMIT: usize = 10; /// Pagination cursor identifying a file by timestamp and UUID. #[derive(Debug, Clone, PartialEq, Eq)] @@ -176,13 +179,13 @@ async fn traverse_directories_for_paths( } // Read head and simultaneously detect message events within the same // first N JSONL records to avoid a second file read. - let (head, saw_session_meta, saw_user_event) = - read_head_and_flags(&path, HEAD_RECORD_LIMIT) + let (head, tail, saw_session_meta, saw_user_event) = + read_head_and_tail(&path, HEAD_RECORD_LIMIT, TAIL_RECORD_LIMIT) .await - .unwrap_or((Vec::new(), false, false)); + .unwrap_or((Vec::new(), Vec::new(), false, false)); // Apply filters: must have session meta and at least one user message event if saw_session_meta && saw_user_event { - items.push(ConversationItem { path, head }); + items.push(ConversationItem { path, head, tail }); } } } @@ -286,10 +289,11 @@ fn parse_timestamp_uuid_from_filename(name: &str) -> Option<(OffsetDateTime, Uui Some((ts, uuid)) } -async fn read_head_and_flags( +async fn read_head_and_tail( path: &Path, - max_records: usize, -) -> io::Result<(Vec, bool, bool)> { + head_limit: usize, + tail_limit: usize, +) -> io::Result<(Vec, Vec, bool, bool)> { use tokio::io::AsyncBufReadExt; let file = tokio::fs::File::open(path).await?; @@ -299,7 +303,7 @@ async fn read_head_and_flags( let mut saw_session_meta = false; let mut saw_user_event = false; - while head.len() < max_records { + while head.len() < head_limit { let line_opt = lines.next_line().await?; let Some(line) = line_opt else { break }; let trimmed = line.trim(); @@ -336,7 +340,84 @@ async fn read_head_and_flags( } } - Ok((head, saw_session_meta, saw_user_event)) + let tail = if tail_limit == 0 { + Vec::new() + } else { + read_tail_records(path, tail_limit).await? + }; + + Ok((head, tail, saw_session_meta, saw_user_event)) +} + +async fn read_tail_records(path: &Path, max_records: usize) -> io::Result> { + use std::io::SeekFrom; + use tokio::io::AsyncReadExt; + use tokio::io::AsyncSeekExt; + + if max_records == 0 { + return Ok(Vec::new()); + } + + const CHUNK_SIZE: usize = 8192; + + let mut file = tokio::fs::File::open(path).await?; + let mut pos = file.seek(SeekFrom::End(0)).await?; + if pos == 0 { + return Ok(Vec::new()); + } + + let mut buffer: Vec = Vec::new(); + + loop { + let slice_start = match (pos > 0, buffer.iter().position(|&b| b == b'\n')) { + (true, Some(idx)) => idx + 1, + _ => 0, + }; + let tail = collect_last_response_values(&buffer[slice_start..], max_records); + if tail.len() >= max_records || pos == 0 { + return Ok(tail); + } + + let read_size = CHUNK_SIZE.min(pos as usize); + if read_size == 0 { + return Ok(tail); + } + pos -= read_size as u64; + file.seek(SeekFrom::Start(pos)).await?; + let mut chunk = vec![0; read_size]; + file.read_exact(&mut chunk).await?; + chunk.extend_from_slice(&buffer); + buffer = chunk; + } +} + +fn collect_last_response_values(buffer: &[u8], max_records: usize) -> Vec { + use std::borrow::Cow; + + if buffer.is_empty() || max_records == 0 { + return Vec::new(); + } + + let text: Cow<'_, str> = String::from_utf8_lossy(buffer); + let mut collected_rev: Vec = Vec::new(); + for line in text.lines().rev() { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + let parsed: serde_json::Result = serde_json::from_str(trimmed); + let Ok(rollout_line) = parsed else { continue }; + if let RolloutItem::ResponseItem(item) = rollout_line.item + && let Ok(val) = serde_json::to_value(item) + { + collected_rev.push(val); + if collected_rev.len() == max_records { + break; + } + } + } + collected_rev.reverse(); + collected_rev } /// Locate a recorded conversation rollout file by its UUID string using the existing diff --git a/codex-rs/core/src/rollout/tests.rs b/codex-rs/core/src/rollout/tests.rs index 86f47645f9..af5234d31b 100644 --- a/codex-rs/core/src/rollout/tests.rs +++ b/codex-rs/core/src/rollout/tests.rs @@ -17,6 +17,18 @@ use crate::rollout::list::ConversationsPage; use crate::rollout::list::Cursor; use crate::rollout::list::get_conversation; use crate::rollout::list::get_conversations; +use anyhow::Result; +use codex_protocol::mcp_protocol::ConversationId; +use codex_protocol::models::ContentItem; +use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::CompactedItem; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::InputMessageKind; +use codex_protocol::protocol::RolloutItem; +use codex_protocol::protocol::RolloutLine; +use codex_protocol::protocol::SessionMeta; +use codex_protocol::protocol::SessionMetaLine; +use codex_protocol::protocol::UserMessageEvent; fn write_session_file( root: &Path, @@ -146,14 +158,17 @@ async fn test_list_conversations_latest_first() { ConversationItem { path: p1, head: head_3, + tail: Vec::new(), }, ConversationItem { path: p2, head: head_2, + tail: Vec::new(), }, ConversationItem { path: p3, head: head_1, + tail: Vec::new(), }, ], next_cursor: Some(expected_cursor), @@ -219,10 +234,12 @@ async fn test_pagination_cursor() { ConversationItem { path: p5, head: head_5, + tail: Vec::new(), }, ConversationItem { path: p4, head: head_4, + tail: Vec::new(), }, ], next_cursor: Some(expected_cursor1.clone()), @@ -269,10 +286,12 @@ async fn test_pagination_cursor() { ConversationItem { path: p3, head: head_3, + tail: Vec::new(), }, ConversationItem { path: p2, head: head_2, + tail: Vec::new(), }, ], next_cursor: Some(expected_cursor2.clone()), @@ -304,6 +323,7 @@ async fn test_pagination_cursor() { items: vec![ConversationItem { path: p1, head: head_1, + tail: Vec::new(), }], next_cursor: Some(expected_cursor3), num_scanned_files: 5, // scanned 05, 04 (anchor), 03, 02 (anchor), 01 @@ -346,6 +366,7 @@ async fn test_get_conversation_contents() { items: vec![ConversationItem { path: expected_path, head: expected_head, + tail: Vec::new(), }], next_cursor: Some(expected_cursor), num_scanned_files: 1, @@ -366,6 +387,250 @@ async fn test_get_conversation_contents() { assert_eq!(content, expected_content); } +#[tokio::test] +async fn test_tail_includes_last_response_items() -> Result<()> { + let temp = TempDir::new().unwrap(); + let home = temp.path(); + + let ts = "2025-06-01T08-00-00"; + let uuid = Uuid::from_u128(42); + let day_dir = home.join("sessions").join("2025").join("06").join("01"); + fs::create_dir_all(&day_dir)?; + let file_path = day_dir.join(format!("rollout-{ts}-{uuid}.jsonl")); + let mut file = File::create(&file_path)?; + + let conversation_id = ConversationId::from_string(&uuid.to_string())?; + let meta_line = RolloutLine { + timestamp: ts.to_string(), + item: RolloutItem::SessionMeta(SessionMetaLine { + meta: SessionMeta { + id: conversation_id, + timestamp: ts.to_string(), + instructions: None, + cwd: ".".into(), + originator: "test_originator".into(), + cli_version: "test_version".into(), + }, + git: None, + }), + }; + writeln!(file, "{}", serde_json::to_string(&meta_line)?)?; + + let user_event_line = RolloutLine { + timestamp: ts.to_string(), + item: RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { + message: "hello".into(), + kind: Some(InputMessageKind::Plain), + images: None, + })), + }; + writeln!(file, "{}", serde_json::to_string(&user_event_line)?)?; + + let total_messages = 12usize; + for idx in 0..total_messages { + let response_line = RolloutLine { + timestamp: format!("{ts}-{idx:02}"), + item: RolloutItem::ResponseItem(ResponseItem::Message { + id: None, + role: "assistant".into(), + content: vec![ContentItem::OutputText { + text: format!("reply-{idx}"), + }], + }), + }; + writeln!(file, "{}", serde_json::to_string(&response_line)?)?; + } + drop(file); + + let page = get_conversations(home, 1, None).await?; + let item = page.items.first().expect("conversation item"); + let tail_len = item.tail.len(); + assert_eq!(tail_len, 10usize.min(total_messages)); + + let expected: Vec = (total_messages - tail_len..total_messages) + .map(|idx| { + serde_json::to_value(ResponseItem::Message { + id: None, + role: "assistant".into(), + content: vec![ContentItem::OutputText { + text: format!("reply-{idx}"), + }], + }) + .expect("serialize response item") + }) + .collect(); + + assert_eq!(item.tail, expected); + + Ok(()) +} + +#[tokio::test] +async fn test_tail_handles_short_sessions() -> Result<()> { + let temp = TempDir::new().unwrap(); + let home = temp.path(); + + let ts = "2025-06-02T08-30-00"; + let uuid = Uuid::from_u128(7); + let day_dir = home.join("sessions").join("2025").join("06").join("02"); + fs::create_dir_all(&day_dir)?; + let file_path = day_dir.join(format!("rollout-{ts}-{uuid}.jsonl")); + let mut file = File::create(&file_path)?; + + let conversation_id = ConversationId::from_string(&uuid.to_string())?; + let meta_line = RolloutLine { + timestamp: ts.to_string(), + item: RolloutItem::SessionMeta(SessionMetaLine { + meta: SessionMeta { + id: conversation_id, + timestamp: ts.to_string(), + instructions: None, + cwd: ".".into(), + originator: "test_originator".into(), + cli_version: "test_version".into(), + }, + git: None, + }), + }; + writeln!(file, "{}", serde_json::to_string(&meta_line)?)?; + + let user_event_line = RolloutLine { + timestamp: ts.to_string(), + item: RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { + message: "hi".into(), + kind: Some(InputMessageKind::Plain), + images: None, + })), + }; + writeln!(file, "{}", serde_json::to_string(&user_event_line)?)?; + + for idx in 0..3 { + let response_line = RolloutLine { + timestamp: format!("{ts}-{idx:02}"), + item: RolloutItem::ResponseItem(ResponseItem::Message { + id: None, + role: "assistant".into(), + content: vec![ContentItem::OutputText { + text: format!("short-{idx}"), + }], + }), + }; + writeln!(file, "{}", serde_json::to_string(&response_line)?)?; + } + drop(file); + + let page = get_conversations(home, 1, None).await?; + let tail = &page.items.first().expect("conversation item").tail; + + assert_eq!(tail.len(), 3); + + let expected: Vec = (0..3) + .map(|idx| { + serde_json::to_value(ResponseItem::Message { + id: None, + role: "assistant".into(), + content: vec![ContentItem::OutputText { + text: format!("short-{idx}"), + }], + }) + .expect("serialize response item") + }) + .collect(); + + assert_eq!(tail, &expected); + + Ok(()) +} + +#[tokio::test] +async fn test_tail_skips_trailing_non_responses() -> Result<()> { + let temp = TempDir::new().unwrap(); + let home = temp.path(); + + let ts = "2025-06-03T10-00-00"; + let uuid = Uuid::from_u128(11); + let day_dir = home.join("sessions").join("2025").join("06").join("03"); + fs::create_dir_all(&day_dir)?; + let file_path = day_dir.join(format!("rollout-{ts}-{uuid}.jsonl")); + let mut file = File::create(&file_path)?; + + let conversation_id = ConversationId::from_string(&uuid.to_string())?; + let meta_line = RolloutLine { + timestamp: ts.to_string(), + item: RolloutItem::SessionMeta(SessionMetaLine { + meta: SessionMeta { + id: conversation_id, + timestamp: ts.to_string(), + instructions: None, + cwd: ".".into(), + originator: "test_originator".into(), + cli_version: "test_version".into(), + }, + git: None, + }), + }; + writeln!(file, "{}", serde_json::to_string(&meta_line)?)?; + + let user_event_line = RolloutLine { + timestamp: ts.to_string(), + item: RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { + message: "hello".into(), + kind: Some(InputMessageKind::Plain), + images: None, + })), + }; + writeln!(file, "{}", serde_json::to_string(&user_event_line)?)?; + + for idx in 0..4 { + let response_line = RolloutLine { + timestamp: format!("{ts}-{idx:02}"), + item: RolloutItem::ResponseItem(ResponseItem::Message { + id: None, + role: "assistant".into(), + content: vec![ContentItem::OutputText { + text: format!("response-{idx}"), + }], + }), + }; + writeln!(file, "{}", serde_json::to_string(&response_line)?)?; + } + + let compacted_line = RolloutLine { + timestamp: format!("{ts}-compacted"), + item: RolloutItem::Compacted(CompactedItem { + message: "compacted".into(), + }), + }; + writeln!(file, "{}", serde_json::to_string(&compacted_line)?)?; + + let shutdown_event = RolloutLine { + timestamp: format!("{ts}-shutdown"), + item: RolloutItem::EventMsg(EventMsg::ShutdownComplete), + }; + writeln!(file, "{}", serde_json::to_string(&shutdown_event)?)?; + drop(file); + + let page = get_conversations(home, 1, None).await?; + let tail = &page.items.first().expect("conversation item").tail; + + let expected: Vec = (0..4) + .map(|idx| { + serde_json::to_value(ResponseItem::Message { + id: None, + role: "assistant".into(), + content: vec![ContentItem::OutputText { + text: format!("response-{idx}"), + }], + }) + .expect("serialize response item") + }) + .collect(); + + assert_eq!(tail, &expected); + + Ok(()) +} + #[tokio::test] async fn test_stable_ordering_same_second_pagination() { let temp = TempDir::new().unwrap(); @@ -410,10 +675,12 @@ async fn test_stable_ordering_same_second_pagination() { ConversationItem { path: p3, head: head(u3), + tail: Vec::new(), }, ConversationItem { path: p2, head: head(u2), + tail: Vec::new(), }, ], next_cursor: Some(expected_cursor1.clone()), @@ -436,6 +703,7 @@ async fn test_stable_ordering_same_second_pagination() { items: vec![ConversationItem { path: p1, head: head(u1), + tail: Vec::new(), }], next_cursor: Some(expected_cursor2), num_scanned_files: 3, // scanned u3, u2 (anchor), u1 diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index ead04eecd5..d6297528ce 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -804,6 +804,7 @@ mod tests { ConversationItem { path: PathBuf::from(path), head: head_with_ts_and_user_text(ts, &[preview]), + tail: Vec::new(), } } @@ -863,10 +864,12 @@ mod tests { let a = ConversationItem { path: PathBuf::from("/tmp/a.jsonl"), head: head_with_ts_and_user_text("2025-01-01T00:00:00Z", &["A"]), + tail: Vec::new(), }; let b = ConversationItem { path: PathBuf::from("/tmp/b.jsonl"), head: head_with_ts_and_user_text("2025-01-02T00:00:00Z", &["B"]), + tail: Vec::new(), }; let rows = rows_from_items(vec![a, b]); assert_eq!(rows.len(), 2); From 55b74c95e29f331b504271e847e0077c182dbf30 Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:46:47 -0700 Subject: [PATCH 07/23] =?UTF-8?q?render=20=E2=80=A2=20as=20dim=20(#4467)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2025-09-29 at 3 28 30 PM --- codex-rs/tui/src/exec_cell/render.rs | 2 +- codex-rs/tui/src/history_cell.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/codex-rs/tui/src/exec_cell/render.rs b/codex-rs/tui/src/exec_cell/render.rs index 27cb53d7f2..daf5956595 100644 --- a/codex-rs/tui/src/exec_cell/render.rs +++ b/codex-rs/tui/src/exec_cell/render.rs @@ -200,7 +200,7 @@ impl ExecCell { if self.is_active() { spinner(self.active_start_time()) } else { - "•".into() + "•".dim() }, " ".into(), if self.is_active() { diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index d0e13003ff..5e42e20312 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -166,7 +166,7 @@ impl HistoryCell for ReasoningSummaryCell { word_wrap_lines( &summary_lines, RtOptions::new(width as usize) - .initial_indent("• ".into()) + .initial_indent("• ".dim().into()) .subsequent_indent(" ".into()), ) } @@ -200,7 +200,7 @@ impl HistoryCell for AgentMessageCell { &self.lines, RtOptions::new(width as usize) .initial_indent(if self.is_first_line { - "• ".into() + "• ".dim().into() } else { " ".into() }) @@ -901,7 +901,7 @@ pub(crate) fn new_mcp_tools_output( } pub(crate) fn new_info_event(message: String, hint: Option) -> PlainHistoryCell { - let mut line = vec!["• ".into(), message.into()]; + let mut line = vec!["• ".dim(), message.into()]; if let Some(hint) = hint { line.push(" ".into()); line.push(hint.dark_gray()); @@ -964,7 +964,7 @@ impl HistoryCell for PlanUpdateCell { }; let mut lines: Vec> = vec![]; - lines.push(vec!["• ".into(), "Updated Plan".bold()].into()); + lines.push(vec!["• ".dim(), "Updated Plan".bold()].into()); let mut indented_lines = vec![]; let note = self @@ -1036,7 +1036,7 @@ pub(crate) fn new_proposed_command(command: &[String]) -> PlainHistoryCell { let cmd = strip_bash_lc_and_escape(command); let mut lines: Vec> = Vec::new(); - lines.push(Line::from(vec!["• ".into(), "Proposed Command".bold()])); + lines.push(Line::from(vec!["• ".dim(), "Proposed Command".bold()])); let highlighted_lines = crate::render::highlight::highlight_bash_to_lines(&cmd); let initial_prefix: Span<'static> = " └ ".dim(); From c81baaabdaaae7f207080b2e21d91729baa2dd71 Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:58:15 -0700 Subject: [PATCH 08/23] no background for /command or @file popup (#4469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit before: Screenshot 2025-09-29 at 3 42 53 PM after: Screenshot 2025-09-29 at 3 43 04 PM --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index f9e9dd769d..ed11be9ed9 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -1238,12 +1238,6 @@ impl ChatComposer { impl WidgetRef for ChatComposer { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let [composer_rect, textarea_rect, popup_rect] = self.layout_areas(area); - if !matches!(self.active_popup, ActivePopup::None) { - buf.set_style( - popup_rect, - user_message_style(terminal_palette::default_bg()), - ); - } match &self.active_popup { ActivePopup::Command(popup) => { popup.render_ref(popup_rect, buf); From 80ccec6530ae310a24c6a74bcda5174d8b4a74c9 Mon Sep 17 00:00:00 2001 From: dedrisian-oai Date: Mon, 29 Sep 2025 16:14:37 -0700 Subject: [PATCH 09/23] Custom prompt args (numeric) (#4470) [Cherry picked from /pull/3565] Adds $1, $2, $3, $ARGUMENTS param parsing for custom prompts. --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 290 ++++++++++++++++-- codex-rs/tui/src/bottom_pane/command_popup.rs | 13 +- codex-rs/tui/src/bottom_pane/mod.rs | 1 + codex-rs/tui/src/bottom_pane/prompt_args.rs | 151 +++++++++ docs/prompts.md | 5 + 5 files changed, 421 insertions(+), 39 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/prompt_args.rs diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index ed11be9ed9..852acc2453 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -24,6 +24,10 @@ use super::footer::render_footer; use super::paste_burst::CharDecision; use super::paste_burst::PasteBurst; use crate::bottom_pane::paste_burst::FlushResult; +use crate::bottom_pane::prompt_args::expand_custom_prompt; +use crate::bottom_pane::prompt_args::expand_if_numeric_with_positional_args; +use crate::bottom_pane::prompt_args::parse_slash_name; +use crate::bottom_pane::prompt_args::prompt_has_numeric_placeholders; use crate::slash_command::SlashCommand; use crate::style::user_message_style; use crate::terminal_palette; @@ -387,6 +391,7 @@ impl ChatComposer { let first_line = self.textarea.text().lines().next().unwrap_or(""); popup.on_composer_text_change(first_line.to_string()); if let Some(sel) = popup.selected_item() { + let mut cursor_target: Option = None; match sel { CommandItem::Builtin(cmd) => { let starts_with_cmd = first_line @@ -395,21 +400,27 @@ impl ChatComposer { if !starts_with_cmd { self.textarea.set_text(&format!("/{} ", cmd.command())); } + if !self.textarea.text().is_empty() { + cursor_target = Some(self.textarea.text().len()); + } } CommandItem::UserPrompt(idx) => { - if let Some(name) = popup.prompt_name(idx) { - let starts_with_cmd = - first_line.trim_start().starts_with(&format!("/{name}")); + if let Some(prompt) = popup.prompt(idx) { + let name = prompt.name.clone(); + let starts_with_cmd = first_line + .trim_start() + .starts_with(format!("/{name}").as_str()); if !starts_with_cmd { - self.textarea.set_text(&format!("/{name} ")); + self.textarea.set_text(format!("/{name} ").as_str()); + } + if !self.textarea.text().is_empty() { + cursor_target = Some(self.textarea.text().len()); } } } } - // After completing the command, move cursor to the end. - if !self.textarea.text().is_empty() { - let end = self.textarea.text().len(); - self.textarea.set_cursor(end); + if let Some(pos) = cursor_target { + self.textarea.set_cursor(pos); } } (InputResult::None, true) @@ -419,26 +430,49 @@ impl ChatComposer { modifiers: KeyModifiers::NONE, .. } => { - if let Some(sel) = popup.selected_item() { - // Clear textarea so no residual text remains. + // If the current line starts with a custom prompt name and includes + // positional args for a numeric-style template, expand and submit + // immediately regardless of the popup selection. + let first_line = self.textarea.text().lines().next().unwrap_or(""); + if let Some((name, _rest)) = parse_slash_name(first_line) + && let Some(prompt) = self.custom_prompts.iter().find(|p| p.name == name) + && let Some(expanded) = + expand_if_numeric_with_positional_args(prompt, first_line) + { self.textarea.set_text(""); - // Capture any needed data from popup before clearing it. - let prompt_content = match sel { - CommandItem::UserPrompt(idx) => { - popup.prompt_content(idx).map(str::to_string) - } - _ => None, - }; - // Hide popup since an action has been dispatched. - self.active_popup = ActivePopup::None; + return (InputResult::Submitted(expanded), true); + } + if let Some(sel) = popup.selected_item() { match sel { CommandItem::Builtin(cmd) => { + self.textarea.set_text(""); return (InputResult::Command(cmd), true); } - CommandItem::UserPrompt(_) => { - if let Some(contents) = prompt_content { - return (InputResult::Submitted(contents), true); + CommandItem::UserPrompt(idx) => { + if let Some(prompt) = popup.prompt(idx) { + let has_numeric = prompt_has_numeric_placeholders(&prompt.content); + + if !has_numeric { + // No placeholders at all: auto-submit the literal content + self.textarea.set_text(""); + return (InputResult::Submitted(prompt.content.clone()), true); + } + // Numeric placeholders present. + // If the user already typed positional args on the first line, + // expand immediately and submit; otherwise insert "/name " so + // they can type args. + let first_line = self.textarea.text().lines().next().unwrap_or(""); + if let Some(expanded) = + expand_if_numeric_with_positional_args(prompt, first_line) + { + self.textarea.set_text(""); + return (InputResult::Submitted(expanded), true); + } else { + let text = format!("/{} ", prompt.name); + self.textarea.set_text(&text); + self.textarea.set_cursor(self.textarea.text().len()); + } } return (InputResult::None, true); } @@ -450,6 +484,7 @@ impl ChatComposer { input => self.handle_input_basic(input), } } + #[inline] fn clamp_to_char_boundary(text: &str, pos: usize) -> usize { let mut p = pos.min(text.len()); @@ -714,16 +749,26 @@ impl ChatComposer { .unwrap_or(after_cursor.len()); let end_idx = safe_cursor + end_rel_idx; + // If the path contains whitespace, wrap it in double quotes so the + // local prompt arg parser treats it as a single argument. Avoid adding + // quotes when the path already contains one to keep behavior simple. + let needs_quotes = path.chars().any(char::is_whitespace); + let inserted = if needs_quotes && !path.contains('"') { + format!("\"{path}\"") + } else { + path.to_string() + }; + // Replace the slice `[start_idx, end_idx)` with the chosen path and a trailing space. let mut new_text = - String::with_capacity(text.len() - (end_idx - start_idx) + path.len() + 1); + String::with_capacity(text.len() - (end_idx - start_idx) + inserted.len() + 1); new_text.push_str(&text[..start_idx]); - new_text.push_str(path); + new_text.push_str(&inserted); new_text.push(' '); new_text.push_str(&text[end_idx..]); self.textarea.set_text(&new_text); - let new_cursor = start_idx.saturating_add(path.len()).saturating_add(1); + let new_cursor = start_idx.saturating_add(inserted.len()).saturating_add(1); self.textarea.set_cursor(new_cursor); } @@ -809,6 +854,7 @@ impl ChatComposer { if self .paste_burst .newline_should_insert_instead_of_submit(now) + && !in_slash_context { self.textarea.insert_str("\n"); self.paste_burst.extend_window(now); @@ -828,6 +874,13 @@ impl ChatComposer { // If there is neither text nor attachments, suppress submission entirely. let has_attachments = !self.attached_images.is_empty(); text = text.trim().to_string(); + + if let Some(expanded) = + expand_custom_prompt(&text, &self.custom_prompts).unwrap_or_default() + { + text = expanded; + } + if text.is_empty() && !has_attachments { return (InputResult::None, true); } @@ -1149,18 +1202,43 @@ impl ChatComposer { /// textarea. This must be called after every modification that can change /// the text so the popup is shown/updated/hidden as appropriate. fn sync_command_popup(&mut self) { - let first_line = self.textarea.text().lines().next().unwrap_or(""); - let input_starts_with_slash = first_line.starts_with('/'); + // Determine whether the caret is inside the initial '/name' token on the first line. + let text = self.textarea.text(); + let first_line_end = text.find('\n').unwrap_or(text.len()); + let first_line = &text[..first_line_end]; + let cursor = self.textarea.cursor(); + let caret_on_first_line = cursor <= first_line_end; + + let is_editing_slash_command_name = if first_line.starts_with('/') && caret_on_first_line { + // Compute the end of the initial '/name' token (name may be empty yet). + let token_end = first_line + .char_indices() + .find(|(_, c)| c.is_whitespace()) + .map(|(i, _)| i) + .unwrap_or(first_line.len()); + cursor <= token_end + } else { + false + }; + // If the cursor is currently positioned within an `@token`, prefer the + // file-search popup over the slash popup so users can insert a file path + // as an argument to the command (e.g., "/review @docs/..."). + if Self::current_at_token(&self.textarea).is_some() { + if matches!(self.active_popup, ActivePopup::Command(_)) { + self.active_popup = ActivePopup::None; + } + return; + } match &mut self.active_popup { ActivePopup::Command(popup) => { - if input_starts_with_slash { + if is_editing_slash_command_name { popup.on_composer_text_change(first_line.to_string()); } else { self.active_popup = ActivePopup::None; } } _ => { - if input_starts_with_slash { + if is_editing_slash_command_name { let mut command_popup = CommandPopup::new(self.custom_prompts.clone()); command_popup.on_composer_text_change(first_line.to_string()); self.active_popup = ActivePopup::Command(command_popup); @@ -1298,6 +1376,7 @@ mod tests { use crate::bottom_pane::InputResult; use crate::bottom_pane::chat_composer::AttachedImage; use crate::bottom_pane::chat_composer::LARGE_PASTE_CHAR_THRESHOLD; + use crate::bottom_pane::prompt_args::extract_positional_args_for_prompt_line; use crate::bottom_pane::textarea::TextArea; use tokio::sync::mpsc::unbounded_channel; @@ -1787,6 +1866,18 @@ mod tests { assert!(composer.textarea.is_empty(), "composer should be cleared"); } + #[test] + fn extract_args_supports_quoted_paths_single_arg() { + let args = extract_positional_args_for_prompt_line("/review \"docs/My File.md\"", "review"); + assert_eq!(args, vec!["docs/My File.md".to_string()]); + } + + #[test] + fn extract_args_supports_mixed_quoted_and_unquoted() { + let args = extract_positional_args_for_prompt_line("/cmd \"with spaces\" simple", "cmd"); + assert_eq!(args, vec!["with spaces".to_string(), "simple".to_string()]); + } + #[test] fn slash_tab_completion_moves_cursor_to_end() { use crossterm::event::KeyCode; @@ -2234,7 +2325,7 @@ mod tests { } #[test] - fn selecting_custom_prompt_submits_file_contents() { + fn selecting_custom_prompt_without_args_submits_content() { let prompt_text = "Hello from saved prompt"; let (tx, _rx) = unbounded_channel::(); @@ -2265,6 +2356,145 @@ mod tests { composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_eq!(InputResult::Submitted(prompt_text.to_string()), result); + assert!(composer.textarea.is_empty()); + } + + #[test] + fn selecting_custom_prompt_with_args_expands_placeholders() { + // Support $1..$9 and $ARGUMENTS in prompt content. + let prompt_text = "Header: $1\nArgs: $ARGUMENTS\nNinth: $9\n"; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + composer.set_custom_prompts(vec![CustomPrompt { + name: "my-prompt".to_string(), + path: "/tmp/my-prompt.md".to_string().into(), + content: prompt_text.to_string(), + description: None, + argument_hint: None, + }]); + + // Type the slash command with two args and hit Enter to submit. + type_chars_humanlike( + &mut composer, + &[ + '/', 'm', 'y', '-', 'p', 'r', 'o', 'm', 'p', 't', ' ', 'f', 'o', 'o', ' ', 'b', + 'a', 'r', + ], + ); + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + let expected = "Header: foo\nArgs: foo bar\nNinth: \n".to_string(); + assert_eq!(InputResult::Submitted(expected), result); + } + + #[test] + fn selecting_custom_prompt_with_no_args_inserts_template() { + let prompt_text = "X:$1 Y:$2 All:[$ARGUMENTS]"; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + composer.set_custom_prompts(vec![CustomPrompt { + name: "p".to_string(), + path: "/tmp/p.md".to_string().into(), + content: prompt_text.to_string(), + description: None, + argument_hint: None, + }]); + + type_chars_humanlike(&mut composer, &['/', 'p']); + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + // With no args typed, selecting the prompt inserts the command template + // and does not submit immediately. + assert_eq!(InputResult::None, result); + assert_eq!("/p ", composer.textarea.text()); + } + + #[test] + fn selecting_custom_prompt_preserves_literal_dollar_dollar() { + // '$$' should remain untouched. + let prompt_text = "Cost: $$ and first: $1"; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + composer.set_custom_prompts(vec![CustomPrompt { + name: "price".to_string(), + path: "/tmp/price.md".to_string().into(), + content: prompt_text.to_string(), + description: None, + argument_hint: None, + }]); + + type_chars_humanlike(&mut composer, &['/', 'p', 'r', 'i', 'c', 'e', ' ', 'x']); + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_eq!( + InputResult::Submitted("Cost: $$ and first: x".to_string()), + result + ); + } + + #[test] + fn selecting_custom_prompt_reuses_cached_arguments_join() { + let prompt_text = "First: $ARGUMENTS\nSecond: $ARGUMENTS"; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + composer.set_custom_prompts(vec![CustomPrompt { + name: "repeat".to_string(), + path: "/tmp/repeat.md".to_string().into(), + content: prompt_text.to_string(), + description: None, + argument_hint: None, + }]); + + type_chars_humanlike( + &mut composer, + &[ + '/', 'r', 'e', 'p', 'e', 'a', 't', ' ', 'o', 'n', 'e', ' ', 't', 'w', 'o', + ], + ); + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + let expected = "First: one two\nSecond: one two".to_string(); + assert_eq!(InputResult::Submitted(expected), result); } #[test] diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index f002acfe1e..cda0a8464d 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -53,12 +53,8 @@ impl CommandPopup { self.prompts = prompts; } - pub(crate) fn prompt_name(&self, idx: usize) -> Option<&str> { - self.prompts.get(idx).map(|p| p.name.as_str()) - } - - pub(crate) fn prompt_content(&self, idx: usize) -> Option<&str> { - self.prompts.get(idx).map(|p| p.content.as_str()) + pub(crate) fn prompt(&self, idx: usize) -> Option<&CustomPrompt> { + self.prompts.get(idx) } /// Update the filter string based on the current composer text. The text @@ -218,7 +214,6 @@ impl WidgetRef for CommandPopup { #[cfg(test)] mod tests { use super::*; - use std::string::ToString; #[test] fn filter_includes_init_when_typing_prefix() { @@ -292,7 +287,7 @@ mod tests { let mut prompt_names: Vec = items .into_iter() .filter_map(|it| match it { - CommandItem::UserPrompt(i) => popup.prompt_name(i).map(ToString::to_string), + CommandItem::UserPrompt(i) => popup.prompt(i).map(|p| p.name.clone()), _ => None, }) .collect(); @@ -312,7 +307,7 @@ mod tests { }]); let items = popup.filtered_items(); let has_collision_prompt = items.into_iter().any(|it| match it { - CommandItem::UserPrompt(i) => popup.prompt_name(i) == Some("init"), + CommandItem::UserPrompt(i) => popup.prompt(i).is_some_and(|p| p.name == "init"), _ => false, }); assert!( diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index a01ffb498f..dd72ebaab8 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -26,6 +26,7 @@ pub mod custom_prompt_view; mod file_search_popup; mod footer; mod list_selection_view; +mod prompt_args; pub(crate) use list_selection_view::SelectionViewParams; mod paste_burst; pub mod popup_consts; diff --git a/codex-rs/tui/src/bottom_pane/prompt_args.rs b/codex-rs/tui/src/bottom_pane/prompt_args.rs new file mode 100644 index 0000000000..a0c67b6e29 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/prompt_args.rs @@ -0,0 +1,151 @@ +use codex_protocol::custom_prompts::CustomPrompt; +use shlex::Shlex; + +/// Parse a first-line slash command of the form `/name `. +/// Returns `(name, rest_after_name)` if the line begins with `/` and contains +/// a non-empty name; otherwise returns `None`. +pub fn parse_slash_name(line: &str) -> Option<(&str, &str)> { + let stripped = line.strip_prefix('/')?; + let mut name_end = stripped.len(); + for (idx, ch) in stripped.char_indices() { + if ch.is_whitespace() { + name_end = idx; + break; + } + } + let name = &stripped[..name_end]; + if name.is_empty() { + return None; + } + let rest = stripped[name_end..].trim_start(); + Some((name, rest)) +} + +/// Parse positional arguments using shlex semantics (supports quoted tokens). +pub fn parse_positional_args(rest: &str) -> Vec { + Shlex::new(rest).collect() +} + +/// Expands a message of the form `/name key=value …` using a matching saved prompt. +/// +/// If the text does not start with `/`, or if no prompt named `name` exists, +/// the function returns `Ok(None)`. On success it returns +/// `Ok(Some(expanded))`; otherwise it returns a descriptive error. +pub fn expand_custom_prompt( + text: &str, + custom_prompts: &[CustomPrompt], +) -> Result, ()> { + let Some((name, rest)) = parse_slash_name(text) else { + return Ok(None); + }; + + let prompt = match custom_prompts.iter().find(|p| p.name == name) { + Some(prompt) => prompt, + None => return Ok(None), + }; + // Only support numeric placeholders ($1..$9) and $ARGUMENTS. + if prompt_has_numeric_placeholders(&prompt.content) { + let pos_args: Vec = Shlex::new(rest).collect(); + let expanded = expand_numeric_placeholders(&prompt.content, &pos_args); + return Ok(Some(expanded)); + } + // No recognized placeholders: return the literal content. + Ok(Some(prompt.content.clone())) +} + +/// Detect whether `content` contains numeric placeholders ($1..$9) or `$ARGUMENTS`. +pub fn prompt_has_numeric_placeholders(content: &str) -> bool { + if content.contains("$ARGUMENTS") { + return true; + } + let bytes = content.as_bytes(); + let mut i = 0; + while i + 1 < bytes.len() { + if bytes[i] == b'$' { + let b1 = bytes[i + 1]; + if (b'1'..=b'9').contains(&b1) { + return true; + } + } + i += 1; + } + false +} + +/// Extract positional arguments from a composer first line like "/name a b" for a given prompt name. +/// Returns empty when the command name does not match or when there are no args. +pub fn extract_positional_args_for_prompt_line(line: &str, prompt_name: &str) -> Vec { + let trimmed = line.trim_start(); + let Some(rest) = trimmed.strip_prefix('/') else { + return Vec::new(); + }; + let mut parts = rest.splitn(2, char::is_whitespace); + let cmd = parts.next().unwrap_or(""); + if cmd != prompt_name { + return Vec::new(); + } + let args_str = parts.next().unwrap_or("").trim(); + if args_str.is_empty() { + return Vec::new(); + } + parse_positional_args(args_str) +} + +/// If the prompt only uses numeric placeholders and the first line contains +/// positional args for it, expand and return Some(expanded); otherwise None. +pub fn expand_if_numeric_with_positional_args( + prompt: &CustomPrompt, + first_line: &str, +) -> Option { + if !prompt_has_numeric_placeholders(&prompt.content) { + return None; + } + let args = extract_positional_args_for_prompt_line(first_line, &prompt.name); + if args.is_empty() { + return None; + } + Some(expand_numeric_placeholders(&prompt.content, &args)) +} + +/// Expand `$1..$9` and `$ARGUMENTS` in `content` with values from `args`. +pub fn expand_numeric_placeholders(content: &str, args: &[String]) -> String { + let mut out = String::with_capacity(content.len()); + let mut i = 0; + let mut cached_joined_args: Option = None; + while let Some(off) = content[i..].find('$') { + let j = i + off; + out.push_str(&content[i..j]); + let rest = &content[j..]; + let bytes = rest.as_bytes(); + if bytes.len() >= 2 { + match bytes[1] { + b'$' => { + out.push_str("$$"); + i = j + 2; + continue; + } + b'1'..=b'9' => { + let idx = (bytes[1] - b'1') as usize; + if let Some(val) = args.get(idx) { + out.push_str(val); + } + i = j + 2; + continue; + } + _ => {} + } + } + if rest.len() > "ARGUMENTS".len() && rest[1..].starts_with("ARGUMENTS") { + if !args.is_empty() { + let joined = cached_joined_args.get_or_insert_with(|| args.join(" ")); + out.push_str(joined); + } + i = j + 1 + "ARGUMENTS".len(); + continue; + } + out.push('$'); + i = j + 1; + } + out.push_str(&content[i..]); + out +} diff --git a/docs/prompts.md b/docs/prompts.md index b98240d2ad..5157c4ecea 100644 --- a/docs/prompts.md +++ b/docs/prompts.md @@ -6,6 +6,11 @@ Save frequently used prompts as Markdown files and reuse them quickly from the s - File type: Only Markdown files with the `.md` extension are recognized. - Name: The filename without the `.md` extension becomes the slash entry. For a file named `my-prompt.md`, type `/my-prompt`. - Content: The file contents are sent as your message when you select the item in the slash popup and press Enter. +- Arguments: Local prompts support placeholders in their content: + - `$1..$9` expand to the first nine positional arguments typed after the slash name + - `$ARGUMENTS` expands to all arguments joined by a single space + - `$$` is preserved literally + - Quoted args: Wrap a single argument in double quotes to include spaces, e.g. `/review "docs/My File.md"`. - How to use: - Start a new session (Codex loads custom prompts on session start). - In the composer, type `/` to open the slash popup and begin typing your prompt name. From 98efd352ae6fffd3207bb01c56e6ea280d079bb8 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 29 Sep 2025 16:35:47 -0700 Subject: [PATCH 10/23] reintroduce "? for shortcuts" (#4364) Reverts openai/codex#4362 --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 247 +++++++-- codex-rs/tui/src/bottom_pane/footer.rs | 487 ++++++++++-------- codex-rs/tui/src/bottom_pane/mod.rs | 8 - ...mposer__tests__backspace_after_pastes.snap | 2 +- ...tom_pane__chat_composer__tests__empty.snap | 2 +- ...__tests__footer_mode_ctrl_c_interrupt.snap | 13 + ...poser__tests__footer_mode_ctrl_c_quit.snap | 13 + ...sts__footer_mode_ctrl_c_then_esc_hint.snap | 13 + ...tests__footer_mode_esc_hint_backtrack.snap | 13 + ...ts__footer_mode_esc_hint_from_overlay.snap | 13 + ...r_mode_overlay_then_external_esc_hint.snap | 13 + ...__tests__footer_mode_shortcut_overlay.snap | 15 + ...tom_pane__chat_composer__tests__large.snap | 2 +- ...chat_composer__tests__multiple_pastes.snap | 2 +- ...tom_pane__chat_composer__tests__small.snap | 2 +- ...ooter__tests__footer_ctrl_c_quit_idle.snap | 4 +- ...er__tests__footer_ctrl_c_quit_running.snap | 4 +- ...__footer__tests__footer_esc_hint_idle.snap | 5 + ...footer__tests__footer_esc_hint_primed.snap | 5 + ...oter__tests__footer_shortcuts_default.snap | 4 +- ...tests__footer_shortcuts_shift_and_esc.snap | 6 +- codex-rs/tui/src/chatwidget.rs | 2 - ...exec_and_status_layout_vt100_snapshot.snap | 3 +- ...atwidget__tests__status_widget_active.snap | 3 +- codex-rs/tui/src/key_hint.rs | 26 - 25 files changed, 609 insertions(+), 298 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 852acc2453..9230bf145f 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -1,4 +1,3 @@ -use codex_core::protocol::TokenUsageInfo; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; @@ -19,8 +18,14 @@ use super::chat_composer_history::ChatComposerHistory; use super::command_popup::CommandItem; use super::command_popup::CommandPopup; use super::file_search_popup::FileSearchPopup; +use super::footer::FooterMode; use super::footer::FooterProps; +use super::footer::esc_hint_mode; +use super::footer::footer_height; +use super::footer::prompt_mode; use super::footer::render_footer; +use super::footer::reset_mode_after_activity; +use super::footer::toggle_shortcut_mode; use super::paste_burst::CharDecision; use super::paste_burst::PasteBurst; use crate::bottom_pane::paste_burst::FlushResult; @@ -78,7 +83,6 @@ pub(crate) struct ChatComposer { dismissed_file_popup_token: Option, current_file_query: Option, pending_pastes: Vec<(String, String)>, - token_usage_info: Option, has_focus: bool, attached_images: Vec, placeholder_text: String, @@ -88,6 +92,7 @@ pub(crate) struct ChatComposer { // When true, disables paste-burst logic and inserts characters immediately. disable_paste_burst: bool, custom_prompts: Vec, + footer_mode: FooterMode, } /// Popup state – at most one can be visible at any time. @@ -97,7 +102,7 @@ enum ActivePopup { File(FileSearchPopup), } -const FOOTER_HINT_HEIGHT: u16 = 1; +const FOOTER_SPACING_HEIGHT: u16 = 1; impl ChatComposer { pub fn new( @@ -121,7 +126,6 @@ impl ChatComposer { dismissed_file_popup_token: None, current_file_query: None, pending_pastes: Vec::new(), - token_usage_info: None, has_focus: has_input_focus, attached_images: Vec::new(), placeholder_text, @@ -129,6 +133,7 @@ impl ChatComposer { paste_burst: PasteBurst::default(), disable_paste_burst: false, custom_prompts: Vec::new(), + footer_mode: FooterMode::ShortcutPrompt, }; // Apply configuration via the setter to keep side-effects centralized. this.set_disable_paste_burst(disable_paste_burst); @@ -136,26 +141,41 @@ impl ChatComposer { } pub fn desired_height(&self, width: u16) -> u16 { + let footer_props = self.footer_props(); + let footer_hint_height = footer_height(footer_props); + let footer_spacing = if footer_hint_height > 0 { + FOOTER_SPACING_HEIGHT + } else { + 0 + }; + let footer_total_height = footer_hint_height + footer_spacing; self.textarea .desired_height(width.saturating_sub(LIVE_PREFIX_COLS)) + 2 + match &self.active_popup { - ActivePopup::None => FOOTER_HINT_HEIGHT, + ActivePopup::None => footer_total_height, ActivePopup::Command(c) => c.calculate_required_height(width), ActivePopup::File(c) => c.calculate_required_height(), } } fn layout_areas(&self, area: Rect) -> [Rect; 3] { + let footer_props = self.footer_props(); + let footer_hint_height = footer_height(footer_props); + let footer_spacing = if footer_hint_height > 0 { + FOOTER_SPACING_HEIGHT + } else { + 0 + }; + let footer_total_height = footer_hint_height + footer_spacing; let popup_constraint = match &self.active_popup { ActivePopup::Command(popup) => { Constraint::Max(popup.calculate_required_height(area.width)) } ActivePopup::File(popup) => Constraint::Max(popup.calculate_required_height()), - ActivePopup::None => Constraint::Max(FOOTER_HINT_HEIGHT), + ActivePopup::None => Constraint::Max(footer_total_height), }; let mut area = area; - // Leave an empty row at the top, unless there isn't room. if area.height > 1 { area.height -= 1; area.y += 1; @@ -179,13 +199,6 @@ impl ChatComposer { self.textarea.is_empty() } - /// Update the cached *context-left* percentage and refresh the placeholder - /// text. The UI relies on the placeholder to convey the remaining - /// context when the composer is empty. - pub(crate) fn set_token_usage(&mut self, token_info: Option) { - self.token_usage_info = token_info; - } - /// Record the history metadata advertised by `SessionConfiguredEvent` so /// that the composer can navigate cross-session history. pub(crate) fn set_history_metadata(&mut self, log_id: u64, entry_count: usize) { @@ -323,6 +336,11 @@ impl ChatComposer { pub fn set_ctrl_c_quit_hint(&mut self, show: bool, has_focus: bool) { self.ctrl_c_quit_hint = show; + if show { + self.footer_mode = prompt_mode(); + } else { + self.footer_mode = reset_mode_after_activity(self.footer_mode); + } self.set_has_focus(has_focus); } @@ -358,6 +376,18 @@ impl ChatComposer { /// Handle key event when the slash-command popup is visible. fn handle_key_event_with_slash_popup(&mut self, key_event: KeyEvent) -> (InputResult, bool) { + if self.handle_shortcut_overlay_key(&key_event) { + return (InputResult::None, true); + } + if key_event.code == KeyCode::Esc { + let next_mode = esc_hint_mode(self.footer_mode, self.is_task_running); + if next_mode != self.footer_mode { + self.footer_mode = next_mode; + return (InputResult::None, true); + } + } else { + self.footer_mode = reset_mode_after_activity(self.footer_mode); + } let ActivePopup::Command(popup) = &mut self.active_popup else { unreachable!(); }; @@ -513,6 +543,18 @@ impl ChatComposer { /// Handle key events when file search popup is visible. fn handle_key_event_with_file_popup(&mut self, key_event: KeyEvent) -> (InputResult, bool) { + if self.handle_shortcut_overlay_key(&key_event) { + return (InputResult::None, true); + } + if key_event.code == KeyCode::Esc { + let next_mode = esc_hint_mode(self.footer_mode, self.is_task_running); + if next_mode != self.footer_mode { + self.footer_mode = next_mode; + return (InputResult::None, true); + } + } else { + self.footer_mode = reset_mode_after_activity(self.footer_mode); + } let ActivePopup::File(popup) = &mut self.active_popup else { unreachable!(); }; @@ -774,6 +816,18 @@ impl ChatComposer { /// Handle key event when no popup is visible. fn handle_key_event_without_popup(&mut self, key_event: KeyEvent) -> (InputResult, bool) { + if self.handle_shortcut_overlay_key(&key_event) { + return (InputResult::None, true); + } + if key_event.code == KeyCode::Esc { + let next_mode = esc_hint_mode(self.footer_mode, self.is_task_running); + if next_mode != self.footer_mode { + self.footer_mode = next_mode; + return (InputResult::None, true); + } + } else { + self.footer_mode = reset_mode_after_activity(self.footer_mode); + } match key_event { KeyEvent { code: KeyCode::Char('d'), @@ -925,6 +979,10 @@ impl ChatComposer { let now = Instant::now(); self.handle_paste_burst_flush(now); + if !matches!(input.code, KeyCode::Esc) { + self.footer_mode = reset_mode_after_activity(self.footer_mode); + } + // If we're capturing a burst and receive Enter, accumulate it instead of inserting. if matches!(input.code, KeyCode::Enter) && self.paste_burst.is_active() @@ -1198,6 +1256,47 @@ impl ChatComposer { false } + fn handle_shortcut_overlay_key(&mut self, key_event: &KeyEvent) -> bool { + if key_event.kind != KeyEventKind::Press { + return false; + } + + let toggles = match key_event.code { + KeyCode::Char('?') if key_event.modifiers.is_empty() => true, + KeyCode::BackTab => true, + KeyCode::Tab if key_event.modifiers.contains(KeyModifiers::SHIFT) => true, + _ => false, + }; + + if !toggles { + return false; + } + + let next = toggle_shortcut_mode(self.footer_mode, self.ctrl_c_quit_hint); + let changed = next != self.footer_mode; + self.footer_mode = next; + changed + } + + fn footer_props(&self) -> FooterProps { + FooterProps { + mode: self.footer_mode(), + esc_backtrack_hint: self.esc_backtrack_hint, + use_shift_enter_hint: self.use_shift_enter_hint, + is_task_running: self.is_task_running, + } + } + + fn footer_mode(&self) -> FooterMode { + if matches!(self.footer_mode, FooterMode::EscHint) { + FooterMode::EscHint + } else if self.ctrl_c_quit_hint { + FooterMode::CtrlCReminder + } else { + self.footer_mode + } + } + /// Synchronize `self.command_popup` with the current text in the /// textarea. This must be called after every modification that can change /// the text so the popup is shown/updated/hidden as appropriate. @@ -1306,10 +1405,18 @@ impl ChatComposer { pub fn set_task_running(&mut self, running: bool) { self.is_task_running = running; + if running { + self.footer_mode = prompt_mode(); + } } pub(crate) fn set_esc_backtrack_hint(&mut self, show: bool) { self.esc_backtrack_hint = show; + if show { + self.footer_mode = esc_hint_mode(self.footer_mode, self.is_task_running); + } else { + self.footer_mode = reset_mode_after_activity(self.footer_mode); + } } } @@ -1324,20 +1431,26 @@ impl WidgetRef for ChatComposer { popup.render_ref(popup_rect, buf); } ActivePopup::None => { - let mut hint_rect = popup_rect; - hint_rect.x += 2; - hint_rect.width = hint_rect.width.saturating_sub(2); - render_footer( - hint_rect, - buf, - FooterProps { - ctrl_c_quit_hint: self.ctrl_c_quit_hint, - is_task_running: self.is_task_running, - esc_backtrack_hint: self.esc_backtrack_hint, - use_shift_enter_hint: self.use_shift_enter_hint, - token_usage_info: self.token_usage_info.as_ref(), - }, - ); + let footer_hint_height = footer_height(self.footer_props()); + let footer_spacing = if footer_hint_height > 0 { + FOOTER_SPACING_HEIGHT + } else { + 0 + }; + let hint_rect = if footer_spacing > 0 && footer_hint_height > 0 { + let [_, hint_rect] = Layout::vertical([ + Constraint::Length(footer_spacing), + Constraint::Length(footer_hint_height), + ]) + .areas(popup_rect); + hint_rect + } else { + popup_rect + }; + let mut footer_rect = hint_rect; + footer_rect.x = footer_rect.x.saturating_add(2); + footer_rect.width = footer_rect.width.saturating_sub(2); + render_footer(footer_rect, buf, self.footer_props()); } } let style = user_message_style(terminal_palette::default_bg()); @@ -1376,6 +1489,7 @@ mod tests { use crate::bottom_pane::InputResult; use crate::bottom_pane::chat_composer::AttachedImage; use crate::bottom_pane::chat_composer::LARGE_PASTE_CHAR_THRESHOLD; + use crate::bottom_pane::footer::footer_height; use crate::bottom_pane::prompt_args::extract_positional_args_for_prompt_line; use crate::bottom_pane::textarea::TextArea; use tokio::sync::mpsc::unbounded_channel; @@ -1407,7 +1521,7 @@ mod tests { let mut hint_row: Option<(u16, String)> = None; for y in 0..area.height { let row = row_to_string(y); - if row.contains(" send") { + if row.contains("? for shortcuts") { hint_row = Some((y, row)); break; } @@ -1434,6 +1548,81 @@ mod tests { ); } + fn snapshot_composer_state(name: &str, enhanced_keys_supported: bool, setup: F) + where + F: FnOnce(&mut ChatComposer), + { + use ratatui::Terminal; + use ratatui::backend::TestBackend; + + let width = 100; + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + enhanced_keys_supported, + "Ask Codex to do anything".to_string(), + false, + ); + setup(&mut composer); + let footer_lines = footer_height(composer.footer_props()); + let height = footer_lines + 8; + let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); + terminal + .draw(|f| f.render_widget_ref(composer, f.area())) + .unwrap(); + insta::assert_snapshot!(name, terminal.backend()); + } + + #[test] + fn footer_mode_snapshots() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + snapshot_composer_state("footer_mode_shortcut_overlay", true, |composer| { + composer.set_esc_backtrack_hint(true); + let _ = + composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); + }); + + snapshot_composer_state("footer_mode_ctrl_c_quit", true, |composer| { + composer.set_ctrl_c_quit_hint(true, true); + }); + + snapshot_composer_state("footer_mode_ctrl_c_interrupt", true, |composer| { + composer.set_task_running(true); + composer.set_ctrl_c_quit_hint(true, true); + }); + + snapshot_composer_state("footer_mode_ctrl_c_then_esc_hint", true, |composer| { + composer.set_ctrl_c_quit_hint(true, true); + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + }); + + snapshot_composer_state("footer_mode_esc_hint_from_overlay", true, |composer| { + let _ = + composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + }); + + snapshot_composer_state("footer_mode_esc_hint_backtrack", true, |composer| { + composer.set_esc_backtrack_hint(true); + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + }); + + snapshot_composer_state( + "footer_mode_overlay_then_external_esc_hint", + true, + |composer| { + let _ = composer + .handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); + composer.set_esc_backtrack_hint(true); + }, + ); + } + #[test] fn test_current_at_token_basic_cases() { let test_cases = vec![ diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 77e2c106e5..aa0659ed9f 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -1,180 +1,206 @@ -use codex_core::protocol::TokenUsageInfo; -use codex_protocol::num_format::format_si_suffix; use crossterm::event::KeyCode; use crossterm::event::KeyModifiers; use ratatui::buffer::Buffer; use ratatui::layout::Rect; -use ratatui::style::Color; -use ratatui::style::Modifier; -use ratatui::style::Style; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; use ratatui::widgets::WidgetRef; -use crate::key_hint; - #[derive(Clone, Copy, Debug)] -pub(crate) struct FooterProps<'a> { - pub(crate) ctrl_c_quit_hint: bool, - pub(crate) is_task_running: bool, +pub(crate) struct FooterProps { + pub(crate) mode: FooterMode, pub(crate) esc_backtrack_hint: bool, pub(crate) use_shift_enter_hint: bool, - pub(crate) token_usage_info: Option<&'a TokenUsageInfo>, + pub(crate) is_task_running: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum FooterMode { + CtrlCReminder, + ShortcutPrompt, + ShortcutOverlay, + EscHint, +} + +pub(crate) fn toggle_shortcut_mode(current: FooterMode, ctrl_c_hint: bool) -> FooterMode { + if ctrl_c_hint { + return current; + } + match current { + FooterMode::ShortcutOverlay | FooterMode::CtrlCReminder => FooterMode::ShortcutPrompt, + _ => FooterMode::ShortcutOverlay, + } +} + +pub(crate) fn esc_hint_mode(current: FooterMode, is_task_running: bool) -> FooterMode { + if is_task_running { + current + } else { + FooterMode::EscHint + } +} + +pub(crate) fn reset_mode_after_activity(current: FooterMode) -> FooterMode { + match current { + FooterMode::EscHint | FooterMode::ShortcutOverlay => FooterMode::ShortcutPrompt, + other => other, + } +} + +pub(crate) fn prompt_mode() -> FooterMode { + FooterMode::ShortcutPrompt +} + +pub(crate) fn footer_height(props: FooterProps) -> u16 { + footer_lines(props).len() as u16 +} + +pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps) { + let lines = footer_lines(props); + for (idx, line) in lines.into_iter().enumerate() { + let y = area.y + idx as u16; + if y >= area.y + area.height { + break; + } + let row = Rect::new(area.x, y, area.width, 1); + line.render_ref(row, buf); + } +} + +fn footer_lines(props: FooterProps) -> Vec> { + match props.mode { + FooterMode::CtrlCReminder => { + vec![ctrl_c_reminder_line(CtrlCReminderState { + is_task_running: props.is_task_running, + })] + } + FooterMode::ShortcutPrompt => vec![Line::from(vec!["? for shortcuts".dim()])], + FooterMode::ShortcutOverlay => shortcut_overlay_lines(ShortcutsState { + use_shift_enter_hint: props.use_shift_enter_hint, + esc_backtrack_hint: props.esc_backtrack_hint, + is_task_running: props.is_task_running, + }), + FooterMode::EscHint => { + vec![esc_hint_line(ShortcutsState { + use_shift_enter_hint: props.use_shift_enter_hint, + esc_backtrack_hint: props.esc_backtrack_hint, + is_task_running: props.is_task_running, + })] + } + } } #[derive(Clone, Copy, Debug)] struct CtrlCReminderState { - pub(crate) is_task_running: bool, + is_task_running: bool, } #[derive(Clone, Copy, Debug)] struct ShortcutsState { - pub(crate) use_shift_enter_hint: bool, - pub(crate) esc_backtrack_hint: bool, + use_shift_enter_hint: bool, + esc_backtrack_hint: bool, + is_task_running: bool, } -#[derive(Clone, Copy, Debug)] -enum FooterContent { - Shortcuts(ShortcutsState), - CtrlCReminder(CtrlCReminderState), -} - -pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps<'_>) { - let content = if props.ctrl_c_quit_hint { - FooterContent::CtrlCReminder(CtrlCReminderState { - is_task_running: props.is_task_running, - }) +fn ctrl_c_reminder_line(state: CtrlCReminderState) -> Line<'static> { + let action = if state.is_task_running { + "interrupt" } else { - FooterContent::Shortcuts(ShortcutsState { - use_shift_enter_hint: props.use_shift_enter_hint, - esc_backtrack_hint: props.esc_backtrack_hint, - }) + "quit" }; - - let mut spans = footer_spans(content); - if let Some(token_usage_info) = props.token_usage_info { - append_token_usage_spans(&mut spans, token_usage_info); - } - - let spans = spans - .into_iter() - .map(|span| span.patch_style(Style::default().dim())) - .collect::>(); - Line::from(spans).render_ref(area, buf); + Line::from(vec![ + Span::from(format!(" ctrl + c again to {action}")).dim(), + ]) } -fn footer_spans(content: FooterContent) -> Vec> { - match content { - FooterContent::Shortcuts(state) => shortcuts_spans(state), - FooterContent::CtrlCReminder(state) => ctrl_c_reminder_spans(state), - } +fn esc_hint_line(state: ShortcutsState) -> Line<'static> { + let text = if state.esc_backtrack_hint { + " esc again to edit previous message" + } else { + " esc esc to edit previous message" + }; + Line::from(vec![Span::from(text).dim()]) } -fn append_token_usage_spans(spans: &mut Vec>, token_usage_info: &TokenUsageInfo) { - let token_usage = &token_usage_info.total_token_usage; - spans.push(" ".into()); - spans.push( - Span::from(format!( - "{} tokens used", - format_si_suffix(token_usage.blended_total()) - )) - .style(Style::default().add_modifier(Modifier::DIM)), - ); - - let last_token_usage = &token_usage_info.last_token_usage; - if let Some(context_window) = token_usage_info.model_context_window { - let percent_remaining: u8 = if context_window > 0 { - last_token_usage.percent_of_context_window_remaining(context_window) - } else { - 100 - }; - - let context_style = if percent_remaining < 20 { - Style::default().fg(Color::Yellow) - } else { - Style::default().add_modifier(Modifier::DIM) - }; - spans.push(" ".into()); - spans.push(Span::styled( - format!("{percent_remaining}% context left"), - context_style, - )); - } -} - -fn shortcuts_spans(state: ShortcutsState) -> Vec> { - let mut spans = Vec::new(); +fn shortcut_overlay_lines(state: ShortcutsState) -> Vec> { + let mut rendered = Vec::new(); for descriptor in SHORTCUTS { - if let Some(segment) = descriptor.footer_segment(state) { - if !segment.prefix.is_empty() { - spans.push(segment.prefix.into()); - } - spans.push(segment.binding.span()); - spans.push(segment.label.into()); + if let Some(text) = descriptor.overlay_entry(state) { + rendered.push(text); } } - spans + build_columns(rendered) } -fn ctrl_c_reminder_spans(state: CtrlCReminderState) -> Vec> { - let followup = if state.is_task_running { - " to interrupt" - } else { - " to quit" - }; - vec![ - " ".into(), - key_hint::ctrl('C'), - " again".into(), - followup.into(), - ] +fn build_columns(entries: Vec) -> Vec> { + if entries.is_empty() { + return Vec::new(); + } + + const COLUMNS: usize = 3; + const MAX_PADDED_WIDTHS: [usize; COLUMNS - 1] = [24, 28]; + const MIN_PADDED_WIDTHS: [usize; COLUMNS - 1] = [22, 0]; + + let rows = entries.len().div_ceil(COLUMNS); + let mut column_widths = [0usize; COLUMNS]; + + for (idx, entry) in entries.iter().enumerate() { + let column = idx % COLUMNS; + column_widths[column] = column_widths[column].max(entry.len()); + } + + let mut lines = Vec::new(); + for row in 0..rows { + let mut line = String::from(" "); + for col in 0..COLUMNS { + let idx = row * COLUMNS + col; + if idx >= entries.len() { + continue; + } + let entry = &entries[idx]; + if col < COLUMNS - 1 { + let max_width = MAX_PADDED_WIDTHS[col]; + let mut target_width = column_widths[col]; + target_width = target_width.max(MIN_PADDED_WIDTHS[col]).min(max_width); + let pad_width = target_width + 2; + line.push_str(&format!("{entry: Span<'static> { - self.display.into_span() - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum ShortcutDisplay { - Plain(&'static str), - Ctrl(char), - Shift(char), -} - -impl ShortcutDisplay { - fn into_span(self) -> Span<'static> { - match self { - ShortcutDisplay::Plain(text) => key_hint::plain(text), - ShortcutDisplay::Ctrl(ch) => key_hint::ctrl(ch), - ShortcutDisplay::Shift(ch) => key_hint::shift(ch), - } + fn matches(&self, state: ShortcutsState) -> bool { + self.condition.matches(state) } } @@ -198,49 +224,55 @@ impl DisplayCondition { struct ShortcutDescriptor { id: ShortcutId, bindings: &'static [ShortcutBinding], - footer_label: &'static str, - footer_prefix: &'static str, + prefix: &'static str, + label: &'static str, } impl ShortcutDescriptor { - fn binding_for(&self, state: ShortcutsState) -> Option { - self.bindings - .iter() - .find(|binding| binding.condition.matches(state)) - .copied() + fn binding_for(&self, state: ShortcutsState) -> Option<&'static ShortcutBinding> { + self.bindings.iter().find(|binding| binding.matches(state)) } - fn should_show(&self, state: ShortcutsState) -> bool { - match self.id { - ShortcutId::EditPrevious => state.esc_backtrack_hint, - _ => true, - } - } - - fn footer_segment(&self, state: ShortcutsState) -> Option { - if !self.should_show(state) { - return None; - } + fn overlay_entry(&self, state: ShortcutsState) -> Option { let binding = self.binding_for(state)?; - Some(FooterSegment { - prefix: self.footer_prefix, - binding, - label: self.footer_label, - }) + let label = match self.id { + ShortcutId::Quit => { + if state.is_task_running { + " to interrupt" + } else { + self.label + } + } + ShortcutId::EditPrevious => { + if state.esc_backtrack_hint { + " again to edit previous message" + } else { + " esc to edit previous message" + } + } + _ => self.label, + }; + let text = match self.id { + ShortcutId::Quit if state.is_task_running => { + format!("{}{} to interrupt", self.prefix, binding.overlay_text) + } + _ => format!("{}{}{}", self.prefix, binding.overlay_text, label), + }; + Some(text) } } const SHORTCUTS: &[ShortcutDescriptor] = &[ ShortcutDescriptor { - id: ShortcutId::Send, + id: ShortcutId::Commands, bindings: &[ShortcutBinding { - code: KeyCode::Enter, + code: KeyCode::Char('/'), modifiers: KeyModifiers::NONE, - display: ShortcutDisplay::Plain("⏎"), + overlay_text: "/", condition: DisplayCondition::Always, }], - footer_label: " send ", - footer_prefix: "", + prefix: "", + label: " for commands", }, ShortcutDescriptor { id: ShortcutId::InsertNewline, @@ -248,138 +280,165 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[ ShortcutBinding { code: KeyCode::Enter, modifiers: KeyModifiers::SHIFT, - display: ShortcutDisplay::Shift('⏎'), + overlay_text: "shift + enter", condition: DisplayCondition::WhenShiftEnterHint, }, ShortcutBinding { code: KeyCode::Char('j'), modifiers: KeyModifiers::CONTROL, - display: ShortcutDisplay::Ctrl('J'), + overlay_text: "ctrl + j", condition: DisplayCondition::WhenNotShiftEnterHint, }, ], - footer_label: " newline ", - footer_prefix: "", + prefix: "", + label: " for newline", }, ShortcutDescriptor { - id: ShortcutId::ShowTranscript, + id: ShortcutId::ChangeMode, bindings: &[ShortcutBinding { - code: KeyCode::Char('t'), - modifiers: KeyModifiers::CONTROL, - display: ShortcutDisplay::Ctrl('T'), + code: KeyCode::BackTab, + modifiers: KeyModifiers::SHIFT, + overlay_text: "shift + tab", condition: DisplayCondition::Always, }], - footer_label: " transcript ", - footer_prefix: "", + prefix: "", + label: " to change mode", }, ShortcutDescriptor { - id: ShortcutId::Quit, + id: ShortcutId::FilePaths, bindings: &[ShortcutBinding { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::CONTROL, - display: ShortcutDisplay::Ctrl('C'), + code: KeyCode::Char('@'), + modifiers: KeyModifiers::NONE, + overlay_text: "@", condition: DisplayCondition::Always, }], - footer_label: " quit", - footer_prefix: "", + prefix: "", + label: " for file paths", + }, + ShortcutDescriptor { + id: ShortcutId::PasteImage, + bindings: &[ShortcutBinding { + code: KeyCode::Char('v'), + modifiers: KeyModifiers::CONTROL, + overlay_text: "ctrl + v", + condition: DisplayCondition::Always, + }], + prefix: "", + label: " to paste images", }, ShortcutDescriptor { id: ShortcutId::EditPrevious, bindings: &[ShortcutBinding { code: KeyCode::Esc, modifiers: KeyModifiers::NONE, - display: ShortcutDisplay::Plain("Esc"), + overlay_text: "esc", condition: DisplayCondition::Always, }], - footer_label: " edit prev", - footer_prefix: " ", + prefix: "", + label: "", + }, + ShortcutDescriptor { + id: ShortcutId::Quit, + bindings: &[ShortcutBinding { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + overlay_text: "ctrl + c", + condition: DisplayCondition::Always, + }], + prefix: "", + label: " to exit", + }, + ShortcutDescriptor { + id: ShortcutId::ShowTranscript, + bindings: &[ShortcutBinding { + code: KeyCode::Char('t'), + modifiers: KeyModifiers::CONTROL, + overlay_text: "ctrl + t", + condition: DisplayCondition::Always, + }], + prefix: "", + label: " to view transcript", }, ]; #[cfg(test)] mod tests { use super::*; - use codex_core::protocol::TokenUsage; use insta::assert_snapshot; use ratatui::Terminal; use ratatui::backend::TestBackend; - fn snapshot_footer(name: &str, props: FooterProps<'_>) { - let mut terminal = Terminal::new(TestBackend::new(80, 3)).unwrap(); + fn snapshot_footer(name: &str, props: FooterProps) { + let height = footer_height(props).max(1); + let mut terminal = Terminal::new(TestBackend::new(80, height)).unwrap(); terminal .draw(|f| { - let area = Rect::new(0, 0, f.area().width, 1); + let area = Rect::new(0, 0, f.area().width, height); render_footer(area, f.buffer_mut(), props); }) .unwrap(); assert_snapshot!(name, terminal.backend()); } - fn token_usage(total_tokens: u64, last_tokens: u64, context_window: u64) -> TokenUsageInfo { - let usage = TokenUsage { - input_tokens: total_tokens, - cached_input_tokens: 0, - output_tokens: 0, - reasoning_output_tokens: 0, - total_tokens, - }; - let last = TokenUsage { - input_tokens: last_tokens, - cached_input_tokens: 0, - output_tokens: 0, - reasoning_output_tokens: 0, - total_tokens: last_tokens, - }; - TokenUsageInfo { - total_token_usage: usage, - last_token_usage: last, - model_context_window: Some(context_window), - } - } - #[test] fn footer_snapshots() { snapshot_footer( "footer_shortcuts_default", FooterProps { - ctrl_c_quit_hint: false, - is_task_running: false, + mode: FooterMode::ShortcutPrompt, esc_backtrack_hint: false, use_shift_enter_hint: false, - token_usage_info: None, + is_task_running: false, }, ); snapshot_footer( "footer_shortcuts_shift_and_esc", FooterProps { - ctrl_c_quit_hint: false, - is_task_running: false, + mode: FooterMode::ShortcutOverlay, esc_backtrack_hint: true, use_shift_enter_hint: true, - token_usage_info: Some(&token_usage(4_200, 900, 8_000)), + is_task_running: false, }, ); snapshot_footer( "footer_ctrl_c_quit_idle", FooterProps { - ctrl_c_quit_hint: true, - is_task_running: false, + mode: FooterMode::CtrlCReminder, esc_backtrack_hint: false, use_shift_enter_hint: false, - token_usage_info: None, + is_task_running: false, }, ); snapshot_footer( "footer_ctrl_c_quit_running", FooterProps { - ctrl_c_quit_hint: true, - is_task_running: true, + mode: FooterMode::CtrlCReminder, esc_backtrack_hint: false, use_shift_enter_hint: false, - token_usage_info: None, + is_task_running: true, + }, + ); + + snapshot_footer( + "footer_esc_hint_idle", + FooterProps { + mode: FooterMode::EscHint, + esc_backtrack_hint: false, + use_shift_enter_hint: false, + is_task_running: false, + }, + ); + + snapshot_footer( + "footer_esc_hint_primed", + FooterProps { + mode: FooterMode::EscHint, + esc_backtrack_hint: true, + use_shift_enter_hint: false, + is_task_running: false, }, ); } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index dd72ebaab8..fccf565195 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -4,7 +4,6 @@ use std::path::PathBuf; use crate::app_event_sender::AppEventSender; use crate::tui::FrameRequester; use bottom_pane_view::BottomPaneView; -use codex_core::protocol::TokenUsageInfo; use codex_file_search::FileMatch; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; @@ -378,13 +377,6 @@ impl BottomPane { !self.is_task_running && self.view_stack.is_empty() && !self.composer.popup_active() } - /// Update the *context-window remaining* indicator in the composer. This - /// is forwarded directly to the underlying `ChatComposer`. - pub(crate) fn set_token_usage(&mut self, token_info: Option) { - self.composer.set_token_usage(token_info); - self.request_redraw(); - } - pub(crate) fn show_view(&mut self, view: Box) { self.push_view(view); } diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap index 3f57cab0f6..3c84d708b0 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +" ? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap index beccbaf7c7..3bdd7260ff 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +" ? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap new file mode 100644 index 0000000000..6f8ad7b98c --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" ctrl + c again to interrupt " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap new file mode 100644 index 0000000000..f7917277fd --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" ctrl + c again to quit " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap new file mode 100644 index 0000000000..99d263da31 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" esc esc to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap new file mode 100644 index 0000000000..404aa48d8c --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap new file mode 100644 index 0000000000..99d263da31 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" esc esc to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap new file mode 100644 index 0000000000..404aa48d8c --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap new file mode 100644 index 0000000000..9633aab2ae --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap @@ -0,0 +1,15 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" / for commands shift + enter for newline shift + tab to change mode " +" @ for file paths ctrl + v to paste images esc again to edit previous message " +" ctrl + c to exit ctrl + t to view transcript " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap index 263c7cc549..53cfb9d76d 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +" ? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap index beb2d8e464..52474a1fd8 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +" ? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap index 41f0f03d8b..d4c6bdedf6 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +" ? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap index 32482ba1cd..31a1b743b8 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap @@ -2,6 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" ⌃C again to quit " -" " -" " +" ctrl + c again to quit " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap index 8c9c2b0d46..9979372a1b 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap @@ -2,6 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" ⌃C again to interrupt " -" " -" " +" ctrl + c again to interrupt " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap new file mode 100644 index 0000000000..b2333b025f --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/bottom_pane/footer.rs +expression: terminal.backend() +--- +" esc esc to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap new file mode 100644 index 0000000000..20f9b178b4 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/bottom_pane/footer.rs +expression: terminal.backend() +--- +" esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap index 8c8a10df85..aff75fdcdf 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap @@ -2,6 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -"⏎ send ⌃J newline ⌃T transcript ⌃C quit " -" " -" " +"? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap index 9b94d89f98..111c14136a 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap @@ -2,6 +2,6 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -"⏎ send ⇧⏎ newline ⌃T transcript ⌃C quit Esc edit prev 4.20K tokens use" -" " -" " +" / for commands shift + enter for newline shift + tab to change m" +" @ for file paths ctrl + v to paste images esc again to edit previ" +" ctrl + c to exit ctrl + t to view transcript " diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 724feb29f2..9a36addb0a 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -395,7 +395,6 @@ impl ChatWidget { pub(crate) fn set_token_info(&mut self, info: Option) { if info.is_some() { - self.bottom_pane.set_token_usage(info.clone()); self.token_info = info; } } @@ -1975,7 +1974,6 @@ impl ChatWidget { pub(crate) fn clear_token_usage(&mut self) { self.token_info = None; - self.bottom_pane.set_token_usage(None); } pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap index 248a57f8f3..1698f2c77f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap @@ -14,4 +14,5 @@ expression: term.backend().vt100().screen().contents() › Summarize recent commits - ⏎ send ⌃J newline ⌃T transcript ⌃C quit + + ? for shortcuts diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap index 9c94206a44..fcd146282b 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap @@ -8,5 +8,6 @@ expression: terminal.backend() " " "› Ask Codex to do anything " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +" " +" ? for shortcuts " " " diff --git a/codex-rs/tui/src/key_hint.rs b/codex-rs/tui/src/key_hint.rs index 80f91d50b2..f68a493e9c 100644 --- a/codex-rs/tui/src/key_hint.rs +++ b/codex-rs/tui/src/key_hint.rs @@ -10,20 +10,6 @@ const ALT_PREFIX: &str = "⌥"; #[cfg(all(not(test), not(target_os = "macos")))] const ALT_PREFIX: &str = "Alt+"; -#[cfg(test)] -const CTRL_PREFIX: &str = "⌃"; -#[cfg(all(not(test), target_os = "macos"))] -const CTRL_PREFIX: &str = "⌃"; -#[cfg(all(not(test), not(target_os = "macos")))] -const CTRL_PREFIX: &str = "Ctrl+"; - -#[cfg(test)] -const SHIFT_PREFIX: &str = "⇧"; -#[cfg(all(not(test), target_os = "macos"))] -const SHIFT_PREFIX: &str = "⇧"; -#[cfg(all(not(test), not(target_os = "macos")))] -const SHIFT_PREFIX: &str = "Shift+"; - fn key_hint_style() -> Style { Style::default().bold() } @@ -32,18 +18,6 @@ fn modifier_span(prefix: &str, key: impl Display) -> Span<'static> { Span::styled(format!("{prefix}{key}"), key_hint_style()) } -pub(crate) fn ctrl(key: impl Display) -> Span<'static> { - modifier_span(CTRL_PREFIX, key) -} - pub(crate) fn alt(key: impl Display) -> Span<'static> { modifier_span(ALT_PREFIX, key) } - -pub(crate) fn shift(key: impl Display) -> Span<'static> { - modifier_span(SHIFT_PREFIX, key) -} - -pub(crate) fn plain(key: impl Display) -> Span<'static> { - Span::styled(format!("{key}"), key_hint_style()) -} From c64da4ff719fd12ab09a04c8f4c4e8d8527021df Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 29 Sep 2025 17:10:04 -0700 Subject: [PATCH 11/23] Fixes (#4458) Fixing the "? for shortcuts" - Only show the hint when composer is empty - Don't reset footer on new task updates - Reorder the elements - Align the "?" and "/" with overlay on and off Based on #4364 --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 145 ++++++++++---- codex-rs/tui/src/bottom_pane/footer.rs | 179 +++++++++--------- ...mposer__tests__backspace_after_pastes.snap | 2 +- ...tom_pane__chat_composer__tests__empty.snap | 1 + ...__tests__footer_mode_ctrl_c_interrupt.snap | 3 +- ...poser__tests__footer_mode_ctrl_c_quit.snap | 3 +- ...sts__footer_mode_ctrl_c_then_esc_hint.snap | 3 +- ...tests__footer_mode_esc_hint_backtrack.snap | 3 +- ...ts__footer_mode_esc_hint_from_overlay.snap | 3 +- ...ests__footer_mode_hidden_while_typing.snap | 12 ++ ...r_mode_overlay_then_external_esc_hint.snap | 3 +- ...__tests__footer_mode_shortcut_overlay.snap | 8 +- ...tom_pane__chat_composer__tests__large.snap | 2 +- ...chat_composer__tests__multiple_pastes.snap | 2 +- ...tom_pane__chat_composer__tests__small.snap | 2 +- ...ooter__tests__footer_ctrl_c_quit_idle.snap | 1 + ...er__tests__footer_ctrl_c_quit_running.snap | 1 + ...__footer__tests__footer_esc_hint_idle.snap | 1 + ...footer__tests__footer_esc_hint_primed.snap | 1 + ...oter__tests__footer_shortcuts_default.snap | 3 +- ...tests__footer_shortcuts_shift_and_esc.snap | 8 +- ...exec_and_status_layout_vt100_snapshot.snap | 3 - ...atwidget__tests__status_widget_active.snap | 2 +- codex-rs/tui/src/ui_consts.rs | 1 + 24 files changed, 246 insertions(+), 146 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_hidden_while_typing.snap diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 9230bf145f..d72cfc2339 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -22,7 +22,6 @@ use super::footer::FooterMode; use super::footer::FooterProps; use super::footer::esc_hint_mode; use super::footer::footer_height; -use super::footer::prompt_mode; use super::footer::render_footer; use super::footer::reset_mode_after_activity; use super::footer::toggle_shortcut_mode; @@ -102,7 +101,7 @@ enum ActivePopup { File(FileSearchPopup), } -const FOOTER_SPACING_HEIGHT: u16 = 1; +const FOOTER_SPACING_HEIGHT: u16 = 0; impl ChatComposer { pub fn new( @@ -143,11 +142,7 @@ impl ChatComposer { pub fn desired_height(&self, width: u16) -> u16 { let footer_props = self.footer_props(); let footer_hint_height = footer_height(footer_props); - let footer_spacing = if footer_hint_height > 0 { - FOOTER_SPACING_HEIGHT - } else { - 0 - }; + let footer_spacing = Self::footer_spacing(footer_hint_height); let footer_total_height = footer_hint_height + footer_spacing; self.textarea .desired_height(width.saturating_sub(LIVE_PREFIX_COLS)) @@ -162,11 +157,7 @@ impl ChatComposer { fn layout_areas(&self, area: Rect) -> [Rect; 3] { let footer_props = self.footer_props(); let footer_hint_height = footer_height(footer_props); - let footer_spacing = if footer_hint_height > 0 { - FOOTER_SPACING_HEIGHT - } else { - 0 - }; + let footer_spacing = Self::footer_spacing(footer_hint_height); let footer_total_height = footer_hint_height + footer_spacing; let popup_constraint = match &self.active_popup { ActivePopup::Command(popup) => { @@ -188,6 +179,14 @@ impl ChatComposer { [composer_rect, textarea_rect, popup_rect] } + fn footer_spacing(footer_hint_height: u16) -> u16 { + if footer_hint_height == 0 { + 0 + } else { + FOOTER_SPACING_HEIGHT + } + } + pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { let [_, textarea_rect, _] = self.layout_areas(area); let state = *self.textarea_state.borrow(); @@ -337,7 +336,7 @@ impl ChatComposer { pub fn set_ctrl_c_quit_hint(&mut self, show: bool, has_focus: bool) { self.ctrl_c_quit_hint = show; if show { - self.footer_mode = prompt_mode(); + self.footer_mode = FooterMode::CtrlCReminder; } else { self.footer_mode = reset_mode_after_activity(self.footer_mode); } @@ -1261,12 +1260,14 @@ impl ChatComposer { return false; } - let toggles = match key_event.code { - KeyCode::Char('?') if key_event.modifiers.is_empty() => true, - KeyCode::BackTab => true, - KeyCode::Tab if key_event.modifiers.contains(KeyModifiers::SHIFT) => true, - _ => false, - }; + let toggles = matches!( + key_event, + KeyEvent { + code: KeyCode::Char('?'), + modifiers: KeyModifiers::NONE, + .. + } if self.is_empty() + ); if !toggles { return false; @@ -1288,12 +1289,13 @@ impl ChatComposer { } fn footer_mode(&self) -> FooterMode { - if matches!(self.footer_mode, FooterMode::EscHint) { - FooterMode::EscHint - } else if self.ctrl_c_quit_hint { - FooterMode::CtrlCReminder - } else { - self.footer_mode + match self.footer_mode { + FooterMode::EscHint => FooterMode::EscHint, + FooterMode::ShortcutOverlay => FooterMode::ShortcutOverlay, + FooterMode::CtrlCReminder => FooterMode::CtrlCReminder, + FooterMode::ShortcutPrompt if self.ctrl_c_quit_hint => FooterMode::CtrlCReminder, + FooterMode::ShortcutPrompt if !self.is_empty() => FooterMode::Empty, + other => other, } } @@ -1405,9 +1407,6 @@ impl ChatComposer { pub fn set_task_running(&mut self, running: bool) { self.is_task_running = running; - if running { - self.footer_mode = prompt_mode(); - } } pub(crate) fn set_esc_backtrack_hint(&mut self, show: bool) { @@ -1431,12 +1430,9 @@ impl WidgetRef for ChatComposer { popup.render_ref(popup_rect, buf); } ActivePopup::None => { - let footer_hint_height = footer_height(self.footer_props()); - let footer_spacing = if footer_hint_height > 0 { - FOOTER_SPACING_HEIGHT - } else { - 0 - }; + let footer_props = self.footer_props(); + let footer_hint_height = footer_height(footer_props); + let footer_spacing = Self::footer_spacing(footer_hint_height); let hint_rect = if footer_spacing > 0 && footer_hint_height > 0 { let [_, hint_rect] = Layout::vertical([ Constraint::Length(footer_spacing), @@ -1447,10 +1443,7 @@ impl WidgetRef for ChatComposer { } else { popup_rect }; - let mut footer_rect = hint_rect; - footer_rect.x = footer_rect.x.saturating_add(2); - footer_rect.width = footer_rect.width.saturating_sub(2); - render_footer(footer_rect, buf, self.footer_props()); + render_footer(hint_rect, buf, footer_props); } } let style = user_message_style(terminal_palette::default_bg()); @@ -1566,8 +1559,10 @@ mod tests { false, ); setup(&mut composer); - let footer_lines = footer_height(composer.footer_props()); - let height = footer_lines + 8; + let footer_props = composer.footer_props(); + let footer_lines = footer_height(footer_props); + let footer_spacing = ChatComposer::footer_spacing(footer_lines); + let height = footer_lines + footer_spacing + 8; let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); terminal .draw(|f| f.render_widget_ref(composer, f.area())) @@ -1621,6 +1616,76 @@ mod tests { composer.set_esc_backtrack_hint(true); }, ); + + snapshot_composer_state("footer_mode_hidden_while_typing", true, |composer| { + type_chars_humanlike(composer, &['h']); + }); + } + + #[test] + fn question_mark_only_toggles_on_first_char() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + let (result, needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); + assert_eq!(result, InputResult::None); + assert!(needs_redraw, "toggling overlay should request redraw"); + assert_eq!(composer.footer_mode, FooterMode::ShortcutOverlay); + + // Toggle back to prompt mode so subsequent typing captures characters. + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); + assert_eq!(composer.footer_mode, FooterMode::ShortcutPrompt); + + type_chars_humanlike(&mut composer, &['h']); + assert_eq!(composer.textarea.text(), "h"); + assert_eq!(composer.footer_mode(), FooterMode::Empty); + + let (result, needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); + assert_eq!(result, InputResult::None); + assert!(needs_redraw, "typing should still mark the view dirty"); + std::thread::sleep(ChatComposer::recommended_paste_flush_delay()); + let _ = composer.flush_paste_burst_if_due(); + assert_eq!(composer.textarea.text(), "h?"); + assert_eq!(composer.footer_mode, FooterMode::ShortcutPrompt); + assert_eq!(composer.footer_mode(), FooterMode::Empty); + } + + #[test] + fn shortcut_overlay_persists_while_task_running() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); + assert_eq!(composer.footer_mode, FooterMode::ShortcutOverlay); + + composer.set_task_running(true); + + assert_eq!(composer.footer_mode, FooterMode::ShortcutOverlay); + assert_eq!(composer.footer_mode(), FooterMode::ShortcutOverlay); } #[test] diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index aa0659ed9f..06a5b5c7e0 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -1,11 +1,12 @@ +use crate::ui_consts::FOOTER_INDENT_COLS; use crossterm::event::KeyCode; use crossterm::event::KeyModifiers; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; -use ratatui::text::Span; use ratatui::widgets::WidgetRef; +use std::iter; #[derive(Clone, Copy, Debug)] pub(crate) struct FooterProps { @@ -21,12 +22,14 @@ pub(crate) enum FooterMode { ShortcutPrompt, ShortcutOverlay, EscHint, + Empty, } pub(crate) fn toggle_shortcut_mode(current: FooterMode, ctrl_c_hint: bool) -> FooterMode { - if ctrl_c_hint { + if ctrl_c_hint && matches!(current, FooterMode::CtrlCReminder) { return current; } + match current { FooterMode::ShortcutOverlay | FooterMode::CtrlCReminder => FooterMode::ShortcutPrompt, _ => FooterMode::ShortcutOverlay, @@ -43,15 +46,14 @@ pub(crate) fn esc_hint_mode(current: FooterMode, is_task_running: bool) -> Foote pub(crate) fn reset_mode_after_activity(current: FooterMode) -> FooterMode { match current { - FooterMode::EscHint | FooterMode::ShortcutOverlay => FooterMode::ShortcutPrompt, + FooterMode::EscHint + | FooterMode::ShortcutOverlay + | FooterMode::CtrlCReminder + | FooterMode::Empty => FooterMode::ShortcutPrompt, other => other, } } -pub(crate) fn prompt_mode() -> FooterMode { - FooterMode::ShortcutPrompt -} - pub(crate) fn footer_height(props: FooterProps) -> u16 { footer_lines(props).len() as u16 } @@ -70,24 +72,16 @@ pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps) { fn footer_lines(props: FooterProps) -> Vec> { match props.mode { - FooterMode::CtrlCReminder => { - vec![ctrl_c_reminder_line(CtrlCReminderState { - is_task_running: props.is_task_running, - })] - } - FooterMode::ShortcutPrompt => vec![Line::from(vec!["? for shortcuts".dim()])], + FooterMode::CtrlCReminder => vec![ctrl_c_reminder_line(CtrlCReminderState { + is_task_running: props.is_task_running, + })], + FooterMode::ShortcutPrompt => vec![dim_line(indent_text("? for shortcuts"))], FooterMode::ShortcutOverlay => shortcut_overlay_lines(ShortcutsState { use_shift_enter_hint: props.use_shift_enter_hint, esc_backtrack_hint: props.esc_backtrack_hint, - is_task_running: props.is_task_running, }), - FooterMode::EscHint => { - vec![esc_hint_line(ShortcutsState { - use_shift_enter_hint: props.use_shift_enter_hint, - esc_backtrack_hint: props.esc_backtrack_hint, - is_task_running: props.is_task_running, - })] - } + FooterMode::EscHint => vec![esc_hint_line(props.esc_backtrack_hint)], + FooterMode::Empty => Vec::new(), } } @@ -100,7 +94,6 @@ struct CtrlCReminderState { struct ShortcutsState { use_shift_enter_hint: bool, esc_backtrack_hint: bool, - is_task_running: bool, } fn ctrl_c_reminder_line(state: CtrlCReminderState) -> Line<'static> { @@ -109,28 +102,54 @@ fn ctrl_c_reminder_line(state: CtrlCReminderState) -> Line<'static> { } else { "quit" }; - Line::from(vec![ - Span::from(format!(" ctrl + c again to {action}")).dim(), - ]) + let text = format!("ctrl + c again to {action}"); + dim_line(indent_text(&text)) } -fn esc_hint_line(state: ShortcutsState) -> Line<'static> { - let text = if state.esc_backtrack_hint { - " esc again to edit previous message" +fn esc_hint_line(esc_backtrack_hint: bool) -> Line<'static> { + let text = if esc_backtrack_hint { + "esc again to edit previous message" } else { - " esc esc to edit previous message" + "esc esc to edit previous message" }; - Line::from(vec![Span::from(text).dim()]) + dim_line(indent_text(text)) } fn shortcut_overlay_lines(state: ShortcutsState) -> Vec> { - let mut rendered = Vec::new(); + let mut commands = String::new(); + let mut newline = String::new(); + let mut file_paths = String::new(); + let mut paste_image = String::new(); + let mut edit_previous = String::new(); + let mut quit = String::new(); + let mut show_transcript = String::new(); + for descriptor in SHORTCUTS { if let Some(text) = descriptor.overlay_entry(state) { - rendered.push(text); + match descriptor.id { + ShortcutId::Commands => commands = text, + ShortcutId::InsertNewline => newline = text, + ShortcutId::FilePaths => file_paths = text, + ShortcutId::PasteImage => paste_image = text, + ShortcutId::EditPrevious => edit_previous = text, + ShortcutId::Quit => quit = text, + ShortcutId::ShowTranscript => show_transcript = text, + } } } - build_columns(rendered) + + let ordered = vec![ + commands, + newline, + file_paths, + paste_image, + edit_previous, + quit, + String::new(), + show_transcript, + ]; + + build_columns(ordered) } fn build_columns(entries: Vec) -> Vec> { @@ -138,11 +157,20 @@ fn build_columns(entries: Vec) -> Vec> { return Vec::new(); } - const COLUMNS: usize = 3; - const MAX_PADDED_WIDTHS: [usize; COLUMNS - 1] = [24, 28]; - const MIN_PADDED_WIDTHS: [usize; COLUMNS - 1] = [22, 0]; + const COLUMNS: usize = 2; + const COLUMN_PADDING: [usize; COLUMNS] = [4, 4]; + const COLUMN_GAP: usize = 4; let rows = entries.len().div_ceil(COLUMNS); + let target_len = rows * COLUMNS; + let mut entries = entries; + if entries.len() < target_len { + entries.extend(std::iter::repeat_n( + String::new(), + target_len - entries.len(), + )); + } + let mut column_widths = [0usize; COLUMNS]; for (idx, entry) in entries.iter().enumerate() { @@ -150,39 +178,43 @@ fn build_columns(entries: Vec) -> Vec> { column_widths[column] = column_widths[column].max(entry.len()); } - let mut lines = Vec::new(); - for row in 0..rows { - let mut line = String::from(" "); - for col in 0..COLUMNS { - let idx = row * COLUMNS + col; - if idx >= entries.len() { - continue; - } - let entry = &entries[idx]; - if col < COLUMNS - 1 { - let max_width = MAX_PADDED_WIDTHS[col]; - let mut target_width = column_widths[col]; - target_width = target_width.max(MIN_PADDED_WIDTHS[col]).min(max_width); - let pad_width = target_width + 2; - line.push_str(&format!("{entry: String { + let mut indented = String::with_capacity(FOOTER_INDENT_COLS + text.len()); + indented.extend(iter::repeat_n(' ', FOOTER_INDENT_COLS)); + indented.push_str(text); + indented +} + +fn dim_line(text: String) -> Line<'static> { + Line::from(text).dim() } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ShortcutId { Commands, InsertNewline, - ChangeMode, FilePaths, PasteImage, EditPrevious, @@ -236,13 +268,6 @@ impl ShortcutDescriptor { fn overlay_entry(&self, state: ShortcutsState) -> Option { let binding = self.binding_for(state)?; let label = match self.id { - ShortcutId::Quit => { - if state.is_task_running { - " to interrupt" - } else { - self.label - } - } ShortcutId::EditPrevious => { if state.esc_backtrack_hint { " again to edit previous message" @@ -252,12 +277,7 @@ impl ShortcutDescriptor { } _ => self.label, }; - let text = match self.id { - ShortcutId::Quit if state.is_task_running => { - format!("{}{} to interrupt", self.prefix, binding.overlay_text) - } - _ => format!("{}{}{}", self.prefix, binding.overlay_text, label), - }; + let text = format!("{}{}{}", self.prefix, binding.overlay_text, label); Some(text) } } @@ -293,17 +313,6 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[ prefix: "", label: " for newline", }, - ShortcutDescriptor { - id: ShortcutId::ChangeMode, - bindings: &[ShortcutBinding { - code: KeyCode::BackTab, - modifiers: KeyModifiers::SHIFT, - overlay_text: "shift + tab", - condition: DisplayCondition::Always, - }], - prefix: "", - label: " to change mode", - }, ShortcutDescriptor { id: ShortcutId::FilePaths, bindings: &[ShortcutBinding { diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap index 3c84d708b0..adb764b6c4 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ? for shortcuts " +" " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap index 3bdd7260ff..db92427864 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 1938 expression: terminal.backend() --- " " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap index 6f8ad7b98c..a805fbf9f4 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 1497 expression: terminal.backend() --- " " @@ -10,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ctrl + c again to interrupt " +" ctrl + c again to interrupt " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap index f7917277fd..750ba10192 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 1497 expression: terminal.backend() --- " " @@ -10,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ctrl + c again to quit " +" ctrl + c again to quit " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap index 99d263da31..8c2d2bfd39 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 1497 expression: terminal.backend() --- " " @@ -10,4 +11,4 @@ expression: terminal.backend() " " " " " " -" esc esc to edit previous message " +" esc esc to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap index 404aa48d8c..5ddf39e315 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 1497 expression: terminal.backend() --- " " @@ -10,4 +11,4 @@ expression: terminal.backend() " " " " " " -" esc again to edit previous message " +" esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap index 99d263da31..8c2d2bfd39 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 1497 expression: terminal.backend() --- " " @@ -10,4 +11,4 @@ expression: terminal.backend() " " " " " " -" esc esc to edit previous message " +" esc esc to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_hidden_while_typing.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_hidden_while_typing.snap new file mode 100644 index 0000000000..dfeb98d619 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_hidden_while_typing.snap @@ -0,0 +1,12 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› h " +" " +" " +" " +" " +" " +" " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap index 404aa48d8c..5ddf39e315 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 1497 expression: terminal.backend() --- " " @@ -10,4 +11,4 @@ expression: terminal.backend() " " " " " " -" esc again to edit previous message " +" esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap index 9633aab2ae..170dedc01e 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 1497 expression: terminal.backend() --- " " @@ -10,6 +11,7 @@ expression: terminal.backend() " " " " " " -" / for commands shift + enter for newline shift + tab to change mode " -" @ for file paths ctrl + v to paste images esc again to edit previous message " -" ctrl + c to exit ctrl + t to view transcript " +" / for commands shift + enter for newline " +" @ for file paths ctrl + v to paste images " +" esc again to edit previous message ctrl + c to exit " +" ctrl + t to view transcript " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap index 53cfb9d76d..4237a17ae0 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ? for shortcuts " +" " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap index 52474a1fd8..3edfc2ce22 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ? for shortcuts " +" " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap index d4c6bdedf6..402740b8fa 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ? for shortcuts " +" " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap index 31a1b743b8..817adb66d9 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/footer.rs +assertion_line: 389 expression: terminal.backend() --- " ctrl + c again to quit " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap index 9979372a1b..50bf9b622d 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/footer.rs +assertion_line: 389 expression: terminal.backend() --- " ctrl + c again to interrupt " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap index b2333b025f..172432a38e 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/footer.rs +assertion_line: 389 expression: terminal.backend() --- " esc esc to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap index 20f9b178b4..69d79d5327 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/footer.rs +assertion_line: 389 expression: terminal.backend() --- " esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap index aff75fdcdf..b2de215488 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/footer.rs +assertion_line: 389 expression: terminal.backend() --- -"? for shortcuts " +" ? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap index 111c14136a..ffa4c5b048 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap @@ -1,7 +1,9 @@ --- source: tui/src/bottom_pane/footer.rs +assertion_line: 389 expression: terminal.backend() --- -" / for commands shift + enter for newline shift + tab to change m" -" @ for file paths ctrl + v to paste images esc again to edit previ" -" ctrl + c to exit ctrl + t to view transcript " +" / for commands shift + enter for newline " +" @ for file paths ctrl + v to paste images " +" esc again to edit previous message ctrl + c to exit " +" ctrl + t to view transcript " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap index 1698f2c77f..e3f7f532a7 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap @@ -13,6 +13,3 @@ expression: term.backend().vt100().screen().contents() › Summarize recent commits - - - ? for shortcuts diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap index fcd146282b..dab45b3b91 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap @@ -1,5 +1,6 @@ --- source: tui/src/chatwidget/tests.rs +assertion_line: 1445 expression: terminal.backend() --- " " @@ -8,6 +9,5 @@ expression: terminal.backend() " " "› Ask Codex to do anything " " " -" " " ? for shortcuts " " " diff --git a/codex-rs/tui/src/ui_consts.rs b/codex-rs/tui/src/ui_consts.rs index b249cd5fcf..ebbe4ce366 100644 --- a/codex-rs/tui/src/ui_consts.rs +++ b/codex-rs/tui/src/ui_consts.rs @@ -8,3 +8,4 @@ /// - Status indicator lines begin with this many spaces for alignment. /// - User history lines account for this many columns (e.g., "▌ ") when wrapping. pub(crate) const LIVE_PREFIX_COLS: u16 = 2; +pub(crate) const FOOTER_INDENT_COLS: usize = LIVE_PREFIX_COLS as usize; From bf76258cdc279dfd09f6c06508f8c5f65d427927 Mon Sep 17 00:00:00 2001 From: dedrisian-oai Date: Mon, 29 Sep 2025 17:58:16 -0700 Subject: [PATCH 12/23] Custom prompts begin with `/prompts:` (#4476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2025-09-29 at 4 41 08 PM --- codex-rs/protocol/src/custom_prompts.rs | 6 +++ codex-rs/tui/src/bottom_pane/chat_composer.rs | 47 ++++++++++++++----- codex-rs/tui/src/bottom_pane/command_popup.rs | 9 +++- codex-rs/tui/src/bottom_pane/prompt_args.rs | 18 +++++-- 4 files changed, 61 insertions(+), 19 deletions(-) diff --git a/codex-rs/protocol/src/custom_prompts.rs b/codex-rs/protocol/src/custom_prompts.rs index 7bceb1390f..4839cba3c2 100644 --- a/codex-rs/protocol/src/custom_prompts.rs +++ b/codex-rs/protocol/src/custom_prompts.rs @@ -3,6 +3,12 @@ use serde::Serialize; use std::path::PathBuf; use ts_rs::TS; +/// Base namespace for custom prompt slash commands (without trailing colon). +/// Example usage forms constructed in code: +/// - Command token after '/': `"{PROMPTS_CMD_PREFIX}:name"` +/// - Full slash prefix: `"/{PROMPTS_CMD_PREFIX}:"` +pub const PROMPTS_CMD_PREFIX: &str = "prompts"; + #[derive(Serialize, Deserialize, Debug, Clone, TS)] pub struct CustomPrompt { pub name: String, diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index d72cfc2339..fd301913e7 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -36,6 +36,7 @@ use crate::slash_command::SlashCommand; use crate::style::user_message_style; use crate::terminal_palette; use codex_protocol::custom_prompts::CustomPrompt; +use codex_protocol::custom_prompts::PROMPTS_CMD_PREFIX; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; @@ -438,9 +439,11 @@ impl ChatComposer { let name = prompt.name.clone(); let starts_with_cmd = first_line .trim_start() - .starts_with(format!("/{name}").as_str()); + .starts_with(format!("/{PROMPTS_CMD_PREFIX}:{name}").as_str()); if !starts_with_cmd { - self.textarea.set_text(format!("/{name} ").as_str()); + self.textarea.set_text( + format!("/{PROMPTS_CMD_PREFIX}:{name} ").as_str(), + ); } if !self.textarea.text().is_empty() { cursor_target = Some(self.textarea.text().len()); @@ -464,7 +467,8 @@ impl ChatComposer { // immediately regardless of the popup selection. let first_line = self.textarea.text().lines().next().unwrap_or(""); if let Some((name, _rest)) = parse_slash_name(first_line) - && let Some(prompt) = self.custom_prompts.iter().find(|p| p.name == name) + && let Some(prompt_name) = name.strip_prefix(&format!("{PROMPTS_CMD_PREFIX}:")) + && let Some(prompt) = self.custom_prompts.iter().find(|p| p.name == prompt_name) && let Some(expanded) = expand_if_numeric_with_positional_args(prompt, first_line) { @@ -498,7 +502,8 @@ impl ChatComposer { self.textarea.set_text(""); return (InputResult::Submitted(expanded), true); } else { - let text = format!("/{} ", prompt.name); + let name = prompt.name.clone(); + let text = format!("/{PROMPTS_CMD_PREFIX}:{name} "); self.textarea.set_text(&text); self.textarea.set_cursor(self.textarea.text().len()); } @@ -2122,13 +2127,17 @@ mod tests { #[test] fn extract_args_supports_quoted_paths_single_arg() { - let args = extract_positional_args_for_prompt_line("/review \"docs/My File.md\"", "review"); + let args = extract_positional_args_for_prompt_line( + "/prompts:review \"docs/My File.md\"", + "review", + ); assert_eq!(args, vec!["docs/My File.md".to_string()]); } #[test] fn extract_args_supports_mixed_quoted_and_unquoted() { - let args = extract_positional_args_for_prompt_line("/cmd \"with spaces\" simple", "cmd"); + let args = + extract_positional_args_for_prompt_line("/prompts:cmd \"with spaces\" simple", "cmd"); assert_eq!(args, vec!["with spaces".to_string(), "simple".to_string()]); } @@ -2603,7 +2612,10 @@ mod tests { type_chars_humanlike( &mut composer, - &['/', 'm', 'y', '-', 'p', 'r', 'o', 'm', 'p', 't'], + &[ + '/', 'p', 'r', 'o', 'm', 'p', 't', 's', ':', 'm', 'y', '-', 'p', 'r', 'o', 'm', + 'p', 't', + ], ); let (result, _needs_redraw) = @@ -2640,8 +2652,8 @@ mod tests { type_chars_humanlike( &mut composer, &[ - '/', 'm', 'y', '-', 'p', 'r', 'o', 'm', 'p', 't', ' ', 'f', 'o', 'o', ' ', 'b', - 'a', 'r', + '/', 'p', 'r', 'o', 'm', 'p', 't', 's', ':', 'm', 'y', '-', 'p', 'r', 'o', 'm', + 'p', 't', ' ', 'f', 'o', 'o', ' ', 'b', 'a', 'r', ], ); let (result, _needs_redraw) = @@ -2673,14 +2685,17 @@ mod tests { argument_hint: None, }]); - type_chars_humanlike(&mut composer, &['/', 'p']); + type_chars_humanlike( + &mut composer, + &['/', 'p', 'r', 'o', 'm', 'p', 't', 's', ':', 'p'], + ); let (result, _needs_redraw) = composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); // With no args typed, selecting the prompt inserts the command template // and does not submit immediately. assert_eq!(InputResult::None, result); - assert_eq!("/p ", composer.textarea.text()); + assert_eq!("/prompts:p ", composer.textarea.text()); } #[test] @@ -2706,7 +2721,12 @@ mod tests { argument_hint: None, }]); - type_chars_humanlike(&mut composer, &['/', 'p', 'r', 'i', 'c', 'e', ' ', 'x']); + type_chars_humanlike( + &mut composer, + &[ + '/', 'p', 'r', 'o', 'm', 'p', 't', 's', ':', 'p', 'r', 'i', 'c', 'e', ' ', 'x', + ], + ); let (result, _needs_redraw) = composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); @@ -2741,7 +2761,8 @@ mod tests { type_chars_humanlike( &mut composer, &[ - '/', 'r', 'e', 'p', 'e', 'a', 't', ' ', 'o', 'n', 'e', ' ', 't', 'w', 'o', + '/', 'p', 'r', 'o', 'm', 'p', 't', 's', ':', 'r', 'e', 'p', 'e', 'a', 't', ' ', + 'o', 'n', 'e', ' ', 't', 'w', 'o', ], ); let (result, _needs_redraw) = diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index cda0a8464d..c493958272 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -10,6 +10,7 @@ use crate::slash_command::SlashCommand; use crate::slash_command::built_in_slash_commands; use codex_common::fuzzy_match::fuzzy_match; use codex_protocol::custom_prompts::CustomPrompt; +use codex_protocol::custom_prompts::PROMPTS_CMD_PREFIX; use std::collections::HashSet; /// A selectable item in the popup: either a built-in command or a user prompt. @@ -120,8 +121,12 @@ impl CommandPopup { out.push((CommandItem::Builtin(*cmd), Some(indices), score)); } } + // Support both search styles: + // - Typing "name" should surface "/prompts:name" results. + // - Typing "prompts:name" should also work. for (idx, p) in self.prompts.iter().enumerate() { - if let Some((indices, score)) = fuzzy_match(&p.name, filter) { + let display = format!("{PROMPTS_CMD_PREFIX}:{}", p.name); + if let Some((indices, score)) = fuzzy_match(&display, filter) { out.push((CommandItem::UserPrompt(idx), Some(indices), score)); } } @@ -158,7 +163,7 @@ impl CommandPopup { (format!("/{}", cmd.command()), cmd.description().to_string()) } CommandItem::UserPrompt(i) => ( - format!("/{}", self.prompts[i].name), + format!("/{PROMPTS_CMD_PREFIX}:{}", self.prompts[i].name), "send saved prompt".to_string(), ), }; diff --git a/codex-rs/tui/src/bottom_pane/prompt_args.rs b/codex-rs/tui/src/bottom_pane/prompt_args.rs index a0c67b6e29..51c95d9998 100644 --- a/codex-rs/tui/src/bottom_pane/prompt_args.rs +++ b/codex-rs/tui/src/bottom_pane/prompt_args.rs @@ -1,4 +1,5 @@ use codex_protocol::custom_prompts::CustomPrompt; +use codex_protocol::custom_prompts::PROMPTS_CMD_PREFIX; use shlex::Shlex; /// Parse a first-line slash command of the form `/name `. @@ -26,9 +27,9 @@ pub fn parse_positional_args(rest: &str) -> Vec { Shlex::new(rest).collect() } -/// Expands a message of the form `/name key=value …` using a matching saved prompt. +/// Expands a message of the form `/prompts:name [value] [value] …` using a matching saved prompt. /// -/// If the text does not start with `/`, or if no prompt named `name` exists, +/// If the text does not start with `/prompts:`, or if no prompt named `name` exists, /// the function returns `Ok(None)`. On success it returns /// `Ok(Some(expanded))`; otherwise it returns a descriptive error. pub fn expand_custom_prompt( @@ -39,7 +40,12 @@ pub fn expand_custom_prompt( return Ok(None); }; - let prompt = match custom_prompts.iter().find(|p| p.name == name) { + // Only handle custom prompts when using the explicit prompts prefix with a colon. + let Some(prompt_name) = name.strip_prefix(&format!("{PROMPTS_CMD_PREFIX}:")) else { + return Ok(None); + }; + + let prompt = match custom_prompts.iter().find(|p| p.name == prompt_name) { Some(prompt) => prompt, None => return Ok(None), }; @@ -79,7 +85,11 @@ pub fn extract_positional_args_for_prompt_line(line: &str, prompt_name: &str) -> let Some(rest) = trimmed.strip_prefix('/') else { return Vec::new(); }; - let mut parts = rest.splitn(2, char::is_whitespace); + // Require the explicit prompts prefix for custom prompt invocations. + let Some(after_prefix) = rest.strip_prefix(&format!("{PROMPTS_CMD_PREFIX}:")) else { + return Vec::new(); + }; + let mut parts = after_prefix.splitn(2, char::is_whitespace); let cmd = parts.next().unwrap_or(""); if cmd != prompt_name { return Vec::new(); From 4a80059b1b2a427ab5e0ac742c3657cb6e8bf7ac Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 29 Sep 2025 18:38:04 -0700 Subject: [PATCH 13/23] Add turn.failed and rename session created to thread started (#4478) Don't produce completed when turn failed. --- codex-rs/exec/src/exec_events.rs | 15 +++++-- ...mental_event_processor_with_json_output.rs | 32 +++++++++----- .../tests/event_processor_with_json_output.rs | 43 +++++++++++++++++-- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/codex-rs/exec/src/exec_events.rs b/codex-rs/exec/src/exec_events.rs index 07a9c01586..0681111e81 100644 --- a/codex-rs/exec/src/exec_events.rs +++ b/codex-rs/exec/src/exec_events.rs @@ -6,12 +6,14 @@ use ts_rs::TS; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] #[serde(tag = "type")] pub enum ConversationEvent { - #[serde(rename = "session.created")] - SessionCreated(SessionCreatedEvent), + #[serde(rename = "thread.started")] + ThreadStarted(ThreadStartedEvent), #[serde(rename = "turn.started")] TurnStarted(TurnStartedEvent), #[serde(rename = "turn.completed")] TurnCompleted(TurnCompletedEvent), + #[serde(rename = "turn.failed")] + TurnFailed(TurnFailedEvent), #[serde(rename = "item.started")] ItemStarted(ItemStartedEvent), #[serde(rename = "item.updated")] @@ -23,8 +25,8 @@ pub enum ConversationEvent { } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] -pub struct SessionCreatedEvent { - pub session_id: String, +pub struct ThreadStartedEvent { + pub thread_id: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS, Default)] @@ -35,6 +37,11 @@ pub struct TurnCompletedEvent { pub usage: Usage, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] +pub struct TurnFailedEvent { + pub error: ConversationErrorEvent, +} + /// Minimal usage summary for a turn. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS, Default)] pub struct Usage { diff --git a/codex-rs/exec/src/experimental_event_processor_with_json_output.rs b/codex-rs/exec/src/experimental_event_processor_with_json_output.rs index 55af87a20b..7d4f12f755 100644 --- a/codex-rs/exec/src/experimental_event_processor_with_json_output.rs +++ b/codex-rs/exec/src/experimental_event_processor_with_json_output.rs @@ -20,10 +20,11 @@ use crate::exec_events::ItemUpdatedEvent; use crate::exec_events::PatchApplyStatus; use crate::exec_events::PatchChangeKind; use crate::exec_events::ReasoningItem; -use crate::exec_events::SessionCreatedEvent; +use crate::exec_events::ThreadStartedEvent; use crate::exec_events::TodoItem; use crate::exec_events::TodoListItem; use crate::exec_events::TurnCompletedEvent; +use crate::exec_events::TurnFailedEvent; use crate::exec_events::TurnStartedEvent; use crate::exec_events::Usage; use codex_core::config::Config; @@ -53,6 +54,7 @@ pub struct ExperimentalEventProcessorWithJsonOutput { // Tracks the todo list for the current turn (at most one per turn). running_todo_list: Option, last_total_token_usage: Option, + last_critical_error: Option, } #[derive(Debug, Clone)] @@ -76,6 +78,7 @@ impl ExperimentalEventProcessorWithJsonOutput { running_patch_applies: HashMap::new(), running_todo_list: None, last_total_token_usage: None, + last_critical_error: None, } } @@ -96,9 +99,13 @@ impl ExperimentalEventProcessorWithJsonOutput { } EventMsg::TaskStarted(ev) => self.handle_task_started(ev), EventMsg::TaskComplete(_) => self.handle_task_complete(), - EventMsg::Error(ev) => vec![ConversationEvent::Error(ConversationErrorEvent { - message: ev.message.clone(), - })], + EventMsg::Error(ev) => { + let error = ConversationErrorEvent { + message: ev.message.clone(), + }; + self.last_critical_error = Some(error.clone()); + vec![ConversationEvent::Error(error)] + } EventMsg::StreamError(ev) => vec![ConversationEvent::Error(ConversationErrorEvent { message: ev.message.clone(), })], @@ -119,8 +126,8 @@ impl ExperimentalEventProcessorWithJsonOutput { &self, payload: &SessionConfiguredEvent, ) -> Vec { - vec![ConversationEvent::SessionCreated(SessionCreatedEvent { - session_id: payload.session_id.to_string(), + vec![ConversationEvent::ThreadStarted(ThreadStartedEvent { + thread_id: payload.session_id.to_string(), })] } @@ -296,7 +303,8 @@ impl ExperimentalEventProcessorWithJsonOutput { vec![ConversationEvent::ItemStarted(ItemStartedEvent { item })] } - fn handle_task_started(&self, _: &TaskStartedEvent) -> Vec { + fn handle_task_started(&mut self, _: &TaskStartedEvent) -> Vec { + self.last_critical_error = None; vec![ConversationEvent::TurnStarted(TurnStartedEvent {})] } @@ -325,9 +333,13 @@ impl ExperimentalEventProcessorWithJsonOutput { })); } - items.push(ConversationEvent::TurnCompleted(TurnCompletedEvent { - usage, - })); + if let Some(error) = self.last_critical_error.take() { + items.push(ConversationEvent::TurnFailed(TurnFailedEvent { error })); + } else { + items.push(ConversationEvent::TurnCompleted(TurnCompletedEvent { + usage, + })); + } items } diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index ebfb667bfb..f6f38d3254 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -1,5 +1,6 @@ use codex_core::protocol::AgentMessageEvent; use codex_core::protocol::AgentReasoningEvent; +use codex_core::protocol::ErrorEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::ExecCommandBeginEvent; @@ -21,10 +22,11 @@ use codex_exec::exec_events::ItemUpdatedEvent; use codex_exec::exec_events::PatchApplyStatus; use codex_exec::exec_events::PatchChangeKind; use codex_exec::exec_events::ReasoningItem; -use codex_exec::exec_events::SessionCreatedEvent; +use codex_exec::exec_events::ThreadStartedEvent; use codex_exec::exec_events::TodoItem as ExecTodoItem; use codex_exec::exec_events::TodoListItem as ExecTodoListItem; use codex_exec::exec_events::TurnCompletedEvent; +use codex_exec::exec_events::TurnFailedEvent; use codex_exec::exec_events::TurnStartedEvent; use codex_exec::exec_events::Usage; use codex_exec::experimental_event_processor_with_json_output::ExperimentalEventProcessorWithJsonOutput; @@ -40,7 +42,7 @@ fn event(id: &str, msg: EventMsg) -> Event { } #[test] -fn session_configured_produces_session_created_event() { +fn session_configured_produces_thread_started_event() { let mut ep = ExperimentalEventProcessorWithJsonOutput::new(None); let session_id = codex_protocol::mcp_protocol::ConversationId::from_string( "67e55044-10b1-426f-9247-bb680e5fe0c8", @@ -62,8 +64,8 @@ fn session_configured_produces_session_created_event() { let out = ep.collect_conversation_events(&ev); assert_eq!( out, - vec![ConversationEvent::SessionCreated(SessionCreatedEvent { - session_id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(), + vec![ConversationEvent::ThreadStarted(ThreadStartedEvent { + thread_id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(), })] ); } @@ -334,6 +336,39 @@ fn stream_error_event_produces_error() { ); } +#[test] +fn error_followed_by_task_complete_produces_turn_failed() { + let mut ep = ExperimentalEventProcessorWithJsonOutput::new(None); + + let error_event = event( + "e1", + EventMsg::Error(ErrorEvent { + message: "boom".to_string(), + }), + ); + assert_eq!( + ep.collect_conversation_events(&error_event), + vec![ConversationEvent::Error(ConversationErrorEvent { + message: "boom".to_string(), + })] + ); + + let complete_event = event( + "e2", + EventMsg::TaskComplete(codex_core::protocol::TaskCompleteEvent { + last_agent_message: None, + }), + ); + assert_eq!( + ep.collect_conversation_events(&complete_event), + vec![ConversationEvent::TurnFailed(TurnFailedEvent { + error: ConversationErrorEvent { + message: "boom".to_string(), + }, + })] + ); +} + #[test] fn exec_command_end_success_produces_completed_command_item() { let mut ep = ExperimentalEventProcessorWithJsonOutput::new(None); From 079303091fc5e50c14180ff8922d95ac12a18e4e Mon Sep 17 00:00:00 2001 From: rakesh-oai Date: Mon, 29 Sep 2025 19:34:57 -0700 Subject: [PATCH 14/23] Rakesh/support device auth (#3531) # External (non-OpenAI) Pull Request Requirements Before opening this Pull Request, please read the dedicated "Contributing" markdown file or your PR may be closed: https://github.com/openai/codex/blob/main/docs/contributing.md If your PR conforms to our contribution guidelines, replace this text with a detailed and high quality description of your changes. # test ``` codex-rs % export CODEX_DEVICE_AUTH_BASE_URL=http://localhost:3007 codex-rs % cargo run --bin codex login --experimental_use-device-code Compiling codex-login v0.0.0 (/Users/rakesh/code/codex/codex-rs/login) Compiling codex-mcp-server v0.0.0 (/Users/rakesh/code/codex/codex-rs/mcp-server) Compiling codex-tui v0.0.0 (/Users/rakesh/code/codex/codex-rs/tui) Compiling codex-cli v0.0.0 (/Users/rakesh/code/codex/codex-rs/cli) Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.90s Running `target/debug/codex login --experimental_use-device-code` To authenticate, enter this code when prompted: 6Q27-KBVRF with interval 5 ^C ``` The error in the last line is since the poll endpoint is not yet implemented --- codex-rs/Cargo.lock | 1 + codex-rs/cli/src/login.rs | 27 ++ codex-rs/cli/src/main.rs | 24 +- codex-rs/login/Cargo.toml | 1 + codex-rs/login/src/device_code_auth.rs | 196 ++++++++++++++ codex-rs/login/src/lib.rs | 2 + codex-rs/login/src/server.rs | 18 +- .../login/tests/suite/device_code_login.rs | 248 ++++++++++++++++++ codex-rs/login/tests/suite/mod.rs | 1 + 9 files changed, 510 insertions(+), 8 deletions(-) create mode 100644 codex-rs/login/src/device_code_auth.rs create mode 100644 codex-rs/login/tests/suite/device_code_login.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index b223a2440b..0b811c3655 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -891,6 +891,7 @@ dependencies = [ "url", "urlencoding", "webbrowser", + "wiremock", ] [[package]] diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index f0816d0b29..8dd4fb8333 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -6,6 +6,7 @@ use codex_core::auth::logout; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_login::ServerOptions; +use codex_login::run_device_code_login; use codex_login::run_login_server; use codex_protocol::mcp_protocol::AuthMode; use std::path::PathBuf; @@ -55,6 +56,32 @@ pub async fn run_login_with_api_key( } } +/// Login using the OAuth device code flow. +pub async fn run_login_with_device_code( + cli_config_overrides: CliConfigOverrides, + issuer_base_url: Option, + client_id: Option, +) -> ! { + let config = load_config_or_exit(cli_config_overrides); + let mut opts = ServerOptions::new( + config.codex_home, + client_id.unwrap_or(CLIENT_ID.to_string()), + ); + if let Some(iss) = issuer_base_url { + opts.issuer = iss; + } + match run_device_code_login(opts).await { + Ok(()) => { + eprintln!("Successfully logged in"); + std::process::exit(0); + } + Err(e) => { + eprintln!("Error logging in with device code: {e}"); + std::process::exit(1); + } + } +} + pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { let config = load_config_or_exit(cli_config_overrides); diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index ca248229c7..df0d30aba9 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -10,6 +10,7 @@ use codex_cli::SeatbeltCommand; use codex_cli::login::run_login_status; use codex_cli::login::run_login_with_api_key; use codex_cli::login::run_login_with_chatgpt; +use codex_cli::login::run_login_with_device_code; use codex_cli::login::run_logout; use codex_cli::proto; use codex_common::CliConfigOverrides; @@ -132,6 +133,20 @@ struct LoginCommand { #[arg(long = "api-key", value_name = "API_KEY")] api_key: Option, + /// EXPERIMENTAL: Use device code flow (not yet supported) + /// This feature is experimental and may changed in future releases. + #[arg(long = "experimental_use-device-code", hide = true)] + use_device_code: bool, + + /// EXPERIMENTAL: Use custom OAuth issuer base URL (advanced) + /// Override the OAuth issuer base URL (advanced) + #[arg(long = "experimental_issuer", value_name = "URL", hide = true)] + issuer_base_url: Option, + + /// EXPERIMENTAL: Use custom OAuth client ID (advanced) + #[arg(long = "experimental_client-id", value_name = "CLIENT_ID", hide = true)] + client_id: Option, + #[command(subcommand)] action: Option, } @@ -274,7 +289,14 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() run_login_status(login_cli.config_overrides).await; } None => { - if let Some(api_key) = login_cli.api_key { + if login_cli.use_device_code { + run_login_with_device_code( + login_cli.config_overrides, + login_cli.issuer_base_url, + login_cli.client_id, + ) + .await; + } else if let Some(api_key) = login_cli.api_key { run_login_with_api_key(login_cli.config_overrides, api_key).await; } else { run_login_with_chatgpt(login_cli.config_overrides).await; diff --git a/codex-rs/login/Cargo.toml b/codex-rs/login/Cargo.toml index 5d358361c1..e2a693e9ac 100644 --- a/codex-rs/login/Cargo.toml +++ b/codex-rs/login/Cargo.toml @@ -33,3 +33,4 @@ webbrowser = { workspace = true } anyhow = { workspace = true } core_test_support = { workspace = true } tempfile = { workspace = true } +wiremock = { workspace = true } diff --git a/codex-rs/login/src/device_code_auth.rs b/codex-rs/login/src/device_code_auth.rs new file mode 100644 index 0000000000..0d04a61337 --- /dev/null +++ b/codex-rs/login/src/device_code_auth.rs @@ -0,0 +1,196 @@ +use reqwest::StatusCode; +use serde::Deserialize; +use serde::Serialize; +use serde::de::Deserializer; +use serde::de::{self}; +use std::time::Duration; +use std::time::Instant; + +use crate::pkce::PkceCodes; +use crate::server::ServerOptions; +use std::io::Write; +use std::io::{self}; + +#[derive(Deserialize)] +struct UserCodeResp { + device_auth_id: String, + #[serde(alias = "user_code", alias = "usercode")] + user_code: String, + #[serde(default, deserialize_with = "deserialize_interval")] + interval: u64, +} + +#[derive(Serialize)] +struct UserCodeReq { + client_id: String, +} + +#[derive(Serialize)] +struct TokenPollReq { + device_auth_id: String, + user_code: String, +} + +fn deserialize_interval<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + s.trim() + .parse::() + .map_err(|e| de::Error::custom(format!("invalid u64 string: {e}"))) +} + +#[derive(Deserialize)] +struct CodeSuccessResp { + authorization_code: String, + code_challenge: String, + code_verifier: String, +} + +/// Request the user code and polling interval. +async fn request_user_code( + client: &reqwest::Client, + auth_base_url: &str, + client_id: &str, +) -> std::io::Result { + let url = format!("{auth_base_url}/deviceauth/usercode"); + let body = serde_json::to_string(&UserCodeReq { + client_id: client_id.to_string(), + }) + .map_err(std::io::Error::other)?; + let resp = client + .post(url) + .header("Content-Type", "application/json") + .body(body) + .send() + .await + .map_err(std::io::Error::other)?; + + if !resp.status().is_success() { + return Err(std::io::Error::other(format!( + "device code request failed with status {}", + resp.status() + ))); + } + + let body = resp.text().await.map_err(std::io::Error::other)?; + serde_json::from_str(&body).map_err(std::io::Error::other) +} + +/// Poll token endpoint until a code is issued or timeout occurs. +async fn poll_for_token( + client: &reqwest::Client, + auth_base_url: &str, + device_auth_id: &str, + user_code: &str, + interval: u64, +) -> std::io::Result { + let url = format!("{auth_base_url}/deviceauth/token"); + let max_wait = Duration::from_secs(15 * 60); + let start = Instant::now(); + + loop { + let body = serde_json::to_string(&TokenPollReq { + device_auth_id: device_auth_id.to_string(), + user_code: user_code.to_string(), + }) + .map_err(std::io::Error::other)?; + let resp = client + .post(&url) + .header("Content-Type", "application/json") + .body(body) + .send() + .await + .map_err(std::io::Error::other)?; + + let status = resp.status(); + + if status.is_success() { + return resp.json().await.map_err(std::io::Error::other); + } + + if status == StatusCode::FORBIDDEN || status == StatusCode::NOT_FOUND { + if start.elapsed() >= max_wait { + return Err(std::io::Error::other( + "device auth timed out after 15 minutes", + )); + } + let sleep_for = Duration::from_secs(interval).min(max_wait - start.elapsed()); + tokio::time::sleep(sleep_for).await; + continue; + } + + return Err(std::io::Error::other(format!( + "device auth failed with status {}", + resp.status() + ))); + } +} + +// Helper to print colored text if terminal supports ANSI +fn print_colored_warning_device_code() { + // ANSI escape code for bright yellow + const YELLOW: &str = "\x1b[93m"; + const RESET: &str = "\x1b[0m"; + let warning = "WARN!!! device code authentication has potential risks and\n\ + should be used with caution only in cases where browser support \n\ + is missing. This is prone to attacks.\n\ + \n\ + - This code is valid for 15 minutes.\n\ + - Do not share this code with anyone.\n\ + "; + let mut stdout = io::stdout().lock(); + let _ = write!(stdout, "{YELLOW}{warning}{RESET}"); + let _ = stdout.flush(); +} + +/// Full device code login flow. +pub async fn run_device_code_login(opts: ServerOptions) -> std::io::Result<()> { + let client = reqwest::Client::new(); + let auth_base_url = opts.issuer.trim_end_matches('/').to_owned(); + print_colored_warning_device_code(); + println!("⏳ Generating a new 9-digit device code for authentication...\n"); + let uc = request_user_code(&client, &auth_base_url, &opts.client_id).await?; + + println!( + "To authenticate, visit: {}/deviceauth/authorize and enter code: {}", + opts.issuer.trim_end_matches('/'), + uc.user_code + ); + + let code_resp = poll_for_token( + &client, + &auth_base_url, + &uc.device_auth_id, + &uc.user_code, + uc.interval, + ) + .await?; + + let pkce = PkceCodes { + code_verifier: code_resp.code_verifier, + code_challenge: code_resp.code_challenge, + }; + println!("authorization code received"); + let redirect_uri = format!("{}/deviceauth/callback", opts.issuer.trim_end_matches('/')); + + let tokens = crate::server::exchange_code_for_tokens( + &opts.issuer, + &opts.client_id, + &redirect_uri, + &pkce, + &code_resp.authorization_code, + ) + .await + .map_err(|err| std::io::Error::other(format!("device code exchange failed: {err}")))?; + + crate::server::persist_tokens_async( + &opts.codex_home, + None, + tokens.id_token, + tokens.access_token, + tokens.refresh_token, + ) + .await +} diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index a737af22c8..d5e5836f0f 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -1,6 +1,8 @@ +mod device_code_auth; mod pkce; mod server; +pub use device_code_auth::run_device_code_login; pub use server::LoginServer; pub use server::ServerOptions; pub use server::ShutdownHandle; diff --git a/codex-rs/login/src/server.rs b/codex-rs/login/src/server.rs index a1e5e6c3b0..7df9038bc5 100644 --- a/codex-rs/login/src/server.rs +++ b/codex-rs/login/src/server.rs @@ -393,13 +393,13 @@ fn bind_server(port: u16) -> io::Result { } } -struct ExchangedTokens { - id_token: String, - access_token: String, - refresh_token: String, +pub(crate) struct ExchangedTokens { + pub id_token: String, + pub access_token: String, + pub refresh_token: String, } -async fn exchange_code_for_tokens( +pub(crate) async fn exchange_code_for_tokens( issuer: &str, client_id: &str, redirect_uri: &str, @@ -443,7 +443,7 @@ async fn exchange_code_for_tokens( }) } -async fn persist_tokens_async( +pub(crate) async fn persist_tokens_async( codex_home: &Path, api_key: Option, id_token: String, @@ -562,7 +562,11 @@ fn jwt_auth_claims(jwt: &str) -> serde_json::Map { serde_json::Map::new() } -async fn obtain_api_key(issuer: &str, client_id: &str, id_token: &str) -> io::Result { +pub(crate) async fn obtain_api_key( + issuer: &str, + client_id: &str, + id_token: &str, +) -> io::Result { // Token exchange for an API key access token #[derive(serde::Deserialize)] struct ExchangeResp { diff --git a/codex-rs/login/tests/suite/device_code_login.rs b/codex-rs/login/tests/suite/device_code_login.rs new file mode 100644 index 0000000000..0b63b98a6d --- /dev/null +++ b/codex-rs/login/tests/suite/device_code_login.rs @@ -0,0 +1,248 @@ +#![allow(clippy::unwrap_used)] + +use base64::Engine; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use codex_core::auth::get_auth_file; +use codex_core::auth::try_read_auth_json; +use codex_login::ServerOptions; +use codex_login::run_device_code_login; +use serde_json::json; +use std::sync::Arc; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; +use tempfile::tempdir; +use wiremock::Mock; +use wiremock::MockServer; +use wiremock::Request; +use wiremock::ResponseTemplate; +use wiremock::matchers::method; +use wiremock::matchers::path; + +use core_test_support::skip_if_no_network; + +// ---------- Small helpers ---------- + +fn make_jwt(payload: serde_json::Value) -> String { + let header = json!({ "alg": "none", "typ": "JWT" }); + let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap()); + let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload).unwrap()); + let signature_b64 = URL_SAFE_NO_PAD.encode(b"sig"); + format!("{header_b64}.{payload_b64}.{signature_b64}") +} + +async fn mock_usercode_success(server: &MockServer) { + Mock::given(method("POST")) + .and(path("/deviceauth/usercode")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "device_auth_id": "device-auth-123", + "user_code": "CODE-12345", + // NOTE: Interval is kept 0 in order to avoid waiting for the interval to pass + "interval": "0" + }))) + .mount(server) + .await; +} + +async fn mock_usercode_failure(server: &MockServer, status: u16) { + Mock::given(method("POST")) + .and(path("/deviceauth/usercode")) + .respond_with(ResponseTemplate::new(status)) + .mount(server) + .await; +} + +async fn mock_poll_token_two_step( + server: &MockServer, + counter: Arc, + first_response_status: u16, +) { + let c = counter.clone(); + Mock::given(method("POST")) + .and(path("/deviceauth/token")) + .respond_with(move |_: &Request| { + let attempt = c.fetch_add(1, Ordering::SeqCst); + if attempt == 0 { + ResponseTemplate::new(first_response_status) + } else { + ResponseTemplate::new(200).set_body_json(json!({ + "authorization_code": "poll-code-321", + "code_challenge": "code-challenge-321", + "code_verifier": "code-verifier-321" + })) + } + }) + .expect(2) + .mount(server) + .await; +} + +async fn mock_poll_token_single(server: &MockServer, endpoint: &str, response: ResponseTemplate) { + Mock::given(method("POST")) + .and(path(endpoint)) + .respond_with(response) + .mount(server) + .await; +} + +async fn mock_oauth_token_single(server: &MockServer, jwt: String) { + Mock::given(method("POST")) + .and(path("/oauth/token")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "id_token": jwt.clone(), + "access_token": "access-token-123", + "refresh_token": "refresh-token-123" + }))) + .mount(server) + .await; +} + +fn server_opts(codex_home: &tempfile::TempDir, issuer: String) -> ServerOptions { + let mut opts = ServerOptions::new(codex_home.path().to_path_buf(), "client-id".to_string()); + opts.issuer = issuer; + opts.open_browser = false; + opts +} + +#[tokio::test] +async fn device_code_login_integration_succeeds() { + skip_if_no_network!(); + + let codex_home = tempdir().unwrap(); + let mock_server = MockServer::start().await; + + mock_usercode_success(&mock_server).await; + + mock_poll_token_two_step(&mock_server, Arc::new(AtomicUsize::new(0)), 404).await; + + let jwt = make_jwt(json!({ + "https://api.openai.com/auth": { + "chatgpt_account_id": "acct_321" + } + })); + + mock_oauth_token_single(&mock_server, jwt.clone()).await; + + let issuer = mock_server.uri(); + let opts = server_opts(&codex_home, issuer); + + run_device_code_login(opts) + .await + .expect("device code login integration should succeed"); + + let auth_path = get_auth_file(codex_home.path()); + let auth = try_read_auth_json(&auth_path).expect("auth.json written"); + // assert_eq!(auth.openai_api_key.as_deref(), Some("api-key-321")); + let tokens = auth.tokens.expect("tokens persisted"); + assert_eq!(tokens.access_token, "access-token-123"); + assert_eq!(tokens.refresh_token, "refresh-token-123"); + assert_eq!(tokens.id_token.raw_jwt, jwt); + assert_eq!(tokens.account_id.as_deref(), Some("acct_321")); +} + +#[tokio::test] +async fn device_code_login_integration_handles_usercode_http_failure() { + skip_if_no_network!(); + + let codex_home = tempdir().unwrap(); + let mock_server = MockServer::start().await; + + mock_usercode_failure(&mock_server, 503).await; + + let issuer = mock_server.uri(); + + let opts = server_opts(&codex_home, issuer); + + let err = run_device_code_login(opts) + .await + .expect_err("usercode HTTP failure should bubble up"); + assert!( + err.to_string() + .contains("device code request failed with status"), + "unexpected error: {err:?}" + ); + + let auth_path = get_auth_file(codex_home.path()); + assert!(!auth_path.exists()); +} + +#[tokio::test] +async fn device_code_login_integration_persists_without_api_key_on_exchange_failure() { + skip_if_no_network!(); + + let codex_home = tempdir().unwrap(); + + let mock_server = MockServer::start().await; + + mock_usercode_success(&mock_server).await; + + mock_poll_token_two_step(&mock_server, Arc::new(AtomicUsize::new(0)), 404).await; + + let jwt = make_jwt(json!({})); + + mock_oauth_token_single(&mock_server, jwt.clone()).await; + + let issuer = mock_server.uri(); + + let mut opts = ServerOptions::new(codex_home.path().to_path_buf(), "client-id".to_string()); + opts.issuer = issuer; + opts.open_browser = false; + + run_device_code_login(opts) + .await + .expect("device login should succeed without API key exchange"); + + let auth_path = get_auth_file(codex_home.path()); + let auth = try_read_auth_json(&auth_path).expect("auth.json written"); + assert!(auth.openai_api_key.is_none()); + let tokens = auth.tokens.expect("tokens persisted"); + assert_eq!(tokens.access_token, "access-token-123"); + assert_eq!(tokens.refresh_token, "refresh-token-123"); + assert_eq!(tokens.id_token.raw_jwt, jwt); +} + +#[tokio::test] +async fn device_code_login_integration_handles_error_payload() { + skip_if_no_network!(); + + let codex_home = tempdir().unwrap(); + + // Start WireMock + let mock_server = MockServer::start().await; + + mock_usercode_success(&mock_server).await; + + // // /deviceauth/token → returns error payload with status 401 + mock_poll_token_single( + &mock_server, + "/deviceauth/token", + ResponseTemplate::new(401).set_body_json(json!({ + "error": "authorization_declined", + "error_description": "Denied" + })), + ) + .await; + + // (WireMock will automatically 404 for other paths) + + let issuer = mock_server.uri(); + + let mut opts = ServerOptions::new(codex_home.path().to_path_buf(), "client-id".to_string()); + opts.issuer = issuer; + opts.open_browser = false; + + let err = run_device_code_login(opts) + .await + .expect_err("integration failure path should return error"); + + // Accept either the specific error payload, a 400, or a 404 (since the client may return 404 if the flow is incomplete) + assert!( + err.to_string().contains("authorization_declined") || err.to_string().contains("401"), + "Expected an authorization_declined / 400 / 404 error, got {err:?}" + ); + + let auth_path = get_auth_file(codex_home.path()); + assert!( + !auth_path.exists(), + "auth.json should not be created when device auth fails" + ); +} diff --git a/codex-rs/login/tests/suite/mod.rs b/codex-rs/login/tests/suite/mod.rs index 3259e72434..b84b264bec 100644 --- a/codex-rs/login/tests/suite/mod.rs +++ b/codex-rs/login/tests/suite/mod.rs @@ -1,2 +1,3 @@ // Aggregates all former standalone integration tests as modules. +mod device_code_login; mod login_server_e2e; From 52e591ce60b96c78aa3a0d10c53ad60590af0fb0 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 29 Sep 2025 19:40:08 -0700 Subject: [PATCH 15/23] Add some types and a basic test to the SDK (#4472) --- pnpm-lock.yaml | 2864 +++++++++++++++------ sdk/typescript/.eslintrc.json | 3 +- sdk/typescript/.prettierrc | 2 +- sdk/typescript/jest.config.cjs | 16 + sdk/typescript/package.json | 20 +- sdk/typescript/samples/basic_streaming.ts | 91 + sdk/typescript/src/codex.ts | 25 + sdk/typescript/src/codexOptions.ts | 7 + sdk/typescript/src/events.ts | 52 + sdk/typescript/src/exec.ts | 81 + sdk/typescript/src/index.ts | 31 +- sdk/typescript/src/items.ts | 99 + sdk/typescript/src/thread.ts | 62 + sdk/typescript/tests/index.test.ts | 9 - sdk/typescript/tests/responsesProxy.ts | 173 ++ sdk/typescript/tests/run.test.ts | 133 + sdk/typescript/tests/runStreamed.test.ts | 167 ++ sdk/typescript/tsconfig.json | 9 +- sdk/typescript/vitest.config.ts | 11 - 19 files changed, 3072 insertions(+), 783 deletions(-) create mode 100644 sdk/typescript/jest.config.cjs create mode 100755 sdk/typescript/samples/basic_streaming.ts create mode 100644 sdk/typescript/src/codex.ts create mode 100644 sdk/typescript/src/codexOptions.ts create mode 100644 sdk/typescript/src/events.ts create mode 100644 sdk/typescript/src/exec.ts create mode 100644 sdk/typescript/src/items.ts create mode 100644 sdk/typescript/src/thread.ts delete mode 100644 sdk/typescript/tests/index.test.ts create mode 100644 sdk/typescript/tests/responsesProxy.ts create mode 100644 sdk/typescript/tests/run.test.ts create mode 100644 sdk/typescript/tests/runStreamed.test.ts delete mode 100644 sdk/typescript/vitest.config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28bcb48ede..cbd7c50b32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,18 +19,18 @@ importers: sdk/typescript: devDependencies: + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 '@types/node': specifier: ^20.19.18 version: 20.19.18 '@typescript-eslint/eslint-plugin': - specifier: ^7.18.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2) + specifier: ^8.45.0 + version: 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2) '@typescript-eslint/parser': - specifier: ^7.18.0 - version: 7.18.0(eslint@9.36.0)(typescript@5.9.2) - '@vitest/coverage-v8': - specifier: ^1.6.1 - version: 1.6.1(vitest@1.6.1(@types/node@20.19.18)) + specifier: ^8.45.0 + version: 8.45.0(eslint@9.36.0)(typescript@5.9.2) eslint: specifier: ^9.36.0 version: 9.36.0 @@ -42,25 +42,65 @@ importers: version: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) + version: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) prettier: specifier: ^3.6.2 version: 3.6.2 + ts-jest: + specifier: ^29.3.4 + version: 29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.19.18)(typescript@5.9.2) tsup: specifier: ^8.5.0 version: 8.5.0(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1) typescript: specifier: ^5.9.2 version: 5.9.2 - vitest: - specifier: ^1.6.1 - version: 1.6.1(@types/node@20.19.18) packages: - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} @@ -70,11 +110,118 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.28.4': resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.4': resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} @@ -82,6 +229,10 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@emnapi/core@1.5.0': resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} @@ -91,204 +242,102 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.25.10': resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.10': resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.10': resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.10': resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.10': resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.10': resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.10': resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.10': resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.10': resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.10': resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.10': resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.10': resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.10': resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.10': resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.10': resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.10': resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.10': resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} engines: {node: '>=18'} @@ -301,12 +350,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.10': resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} engines: {node: '>=18'} @@ -319,12 +362,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.10': resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} engines: {node: '>=18'} @@ -337,48 +374,24 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.10': resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.10': resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.10': resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.10': resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} engines: {node: '>=18'} @@ -443,17 +456,86 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -464,6 +546,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -603,12 +688,57 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -618,63 +748,73 @@ packages: '@types/node@20.19.18': resolution: {integrity: sha512-KeYVbfnbsBCyKG8e3gmUqAfyZNcoj/qpEbHRkQkfZdKOBrU7QQ+BsTdfqLSWX9/m1ytYreMhpKvp+EZi3UFYAg==} - '@typescript-eslint/eslint-plugin@7.18.0': - resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} - engines: {node: ^18.18.0 || >=20.0.0} + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@typescript-eslint/eslint-plugin@8.45.0': + resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/parser': ^8.45.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@7.18.0': - resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/parser@8.45.0': + resolution: {integrity: sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@7.18.0': - resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} - engines: {node: ^18.18.0 || >=20.0.0} - - '@typescript-eslint/type-utils@7.18.0': - resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/project-service@8.45.0': + resolution: {integrity: sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@7.18.0': - resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@8.45.0': + resolution: {integrity: sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@7.18.0': - resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/tsconfig-utils@8.45.0': + resolution: {integrity: sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@7.18.0': - resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/type-utils@8.45.0': + resolution: {integrity: sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@7.18.0': - resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/types@8.45.0': + resolution: {integrity: sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.45.0': + resolution: {integrity: sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.45.0': + resolution: {integrity: sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.45.0': + resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -771,26 +911,6 @@ packages: cpu: [x64] os: [win32] - '@vitest/coverage-v8@1.6.1': - resolution: {integrity: sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==} - peerDependencies: - vitest: 1.6.1 - - '@vitest/expect@1.6.1': - resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} - - '@vitest/runner@1.6.1': - resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} - - '@vitest/snapshot@1.6.1': - resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} - - '@vitest/spy@1.6.1': - resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} - - '@vitest/utils@1.6.1': - resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -808,6 +928,10 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -831,6 +955,16 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -842,10 +976,6 @@ packages: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - array.prototype.findlastindex@1.2.6: resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} engines: {node: '>= 0.4'} @@ -862,9 +992,6 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} - assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -873,9 +1000,38 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + baseline-browser-mapping@2.8.9: + resolution: {integrity: sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==} + hasBin: true + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -886,6 +1042,21 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.26.2: + resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -912,21 +1083,47 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - chai@4.5.0: - resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} - engines: {node: '>=4'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001745: + resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -948,6 +1145,17 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -981,13 +1189,21 @@ packages: supports-color: optional: true - deep-eql@4.1.4: - resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} - engines: {node: '>=6'} + dedent@1.7.0: + resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -996,13 +1212,17 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} @@ -1015,12 +1235,22 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.227: + resolution: {integrity: sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-abstract@1.24.0: resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} @@ -1049,16 +1279,19 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.25.10: resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1142,6 +1375,11 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -1154,16 +1392,21 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1181,6 +1424,9 @@ packages: fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1198,6 +1444,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1242,20 +1492,29 @@ packages: resolution: {integrity: sha512-xPypGGincdfyl/AiSGa7GjXLkvld9V7GjZlowup9SHIJnQnHLFiLODCd/DqKOp0PBagbHJ68r1KJI9Mut7m4sA==} engines: {node: '>= 0.4'} - get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} @@ -1288,17 +1547,21 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -1329,18 +1592,27 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -1360,6 +1632,9 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -1403,6 +1678,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + is-generator-function@1.1.1: resolution: {integrity: sha512-Gn8BWUdrTzf9XUJAvqIYP7QnSC3mKs8QjQdGdJ7HmBemzZo14wj/OVmmAwgxDX/7WhFEjboybL4VhXGIQYPlOA==} engines: {node: '>= 0.4'} @@ -1439,9 +1718,9 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} - is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} @@ -1477,12 +1756,20 @@ packages: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@5.0.6: - resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} istanbul-reports@3.2.0: @@ -1492,20 +1779,161 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -1516,9 +1944,22 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1534,36 +1975,42 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - local-pkg@0.5.1: - resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} - engines: {node: '>=14'} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} - magicast@0.3.5: - resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} - make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1579,9 +2026,9 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1619,9 +2066,22 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.21: + resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -1654,9 +2114,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -1666,18 +2126,26 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -1685,6 +2153,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1697,10 +2169,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -1708,19 +2176,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1736,6 +2194,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -1783,10 +2245,17 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1805,6 +2274,14 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1816,6 +2293,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} @@ -1886,13 +2367,16 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -1901,23 +2385,36 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@3.9.0: - resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1950,17 +2447,18 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@2.1.1: - resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} - sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -1970,6 +2468,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -1985,9 +2487,6 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -1995,13 +2494,8 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinypool@0.8.4: - resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} - engines: {node: '>=14.0.0'} - - tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} - engines: {node: '>=14.0.0'} + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -2014,15 +2508,56 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} peerDependencies: - typescript: '>=4.2.0' + typescript: '>=4.8.4' ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-jest@29.4.4: + resolution: {integrity: sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -2052,10 +2587,18 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-detect@4.1.0: - resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -2080,6 +2623,11 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -2090,69 +2638,24 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vite-node@1.6.1: - resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - vite@5.4.20: - resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} - vitest@1.6.1: - resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.6.1 - '@vitest/ui': 1.6.1 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -2181,15 +2684,13 @@ packages: engines: {node: '>= 8'} hasBin: true - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2201,34 +2702,222 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.1: resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} engines: {node: '>= 14.6'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yocto-queue@1.2.1: - resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} - engines: {node: '>=12.20'} - snapshots: - '@ampproject/remapping@2.3.0': + '@babel/code-frame@7.27.1': dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.2 + lru-cache: 5.1.1 + semver: 7.7.2 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + '@babel/parser@7.28.4': dependencies: '@babel/types': 7.28.4 + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.4': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -2236,6 +2925,10 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@emnapi/core@1.5.0': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -2252,150 +2945,81 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.21.5': - optional: true - '@esbuild/aix-ppc64@0.25.10': optional: true - '@esbuild/android-arm64@0.21.5': - optional: true - '@esbuild/android-arm64@0.25.10': optional: true - '@esbuild/android-arm@0.21.5': - optional: true - '@esbuild/android-arm@0.25.10': optional: true - '@esbuild/android-x64@0.21.5': - optional: true - '@esbuild/android-x64@0.25.10': optional: true - '@esbuild/darwin-arm64@0.21.5': - optional: true - '@esbuild/darwin-arm64@0.25.10': optional: true - '@esbuild/darwin-x64@0.21.5': - optional: true - '@esbuild/darwin-x64@0.25.10': optional: true - '@esbuild/freebsd-arm64@0.21.5': - optional: true - '@esbuild/freebsd-arm64@0.25.10': optional: true - '@esbuild/freebsd-x64@0.21.5': - optional: true - '@esbuild/freebsd-x64@0.25.10': optional: true - '@esbuild/linux-arm64@0.21.5': - optional: true - '@esbuild/linux-arm64@0.25.10': optional: true - '@esbuild/linux-arm@0.21.5': - optional: true - '@esbuild/linux-arm@0.25.10': optional: true - '@esbuild/linux-ia32@0.21.5': - optional: true - '@esbuild/linux-ia32@0.25.10': optional: true - '@esbuild/linux-loong64@0.21.5': - optional: true - '@esbuild/linux-loong64@0.25.10': optional: true - '@esbuild/linux-mips64el@0.21.5': - optional: true - '@esbuild/linux-mips64el@0.25.10': optional: true - '@esbuild/linux-ppc64@0.21.5': - optional: true - '@esbuild/linux-ppc64@0.25.10': optional: true - '@esbuild/linux-riscv64@0.21.5': - optional: true - '@esbuild/linux-riscv64@0.25.10': optional: true - '@esbuild/linux-s390x@0.21.5': - optional: true - '@esbuild/linux-s390x@0.25.10': optional: true - '@esbuild/linux-x64@0.21.5': - optional: true - '@esbuild/linux-x64@0.25.10': optional: true '@esbuild/netbsd-arm64@0.25.10': optional: true - '@esbuild/netbsd-x64@0.21.5': - optional: true - '@esbuild/netbsd-x64@0.25.10': optional: true '@esbuild/openbsd-arm64@0.25.10': optional: true - '@esbuild/openbsd-x64@0.21.5': - optional: true - '@esbuild/openbsd-x64@0.25.10': optional: true '@esbuild/openharmony-arm64@0.25.10': optional: true - '@esbuild/sunos-x64@0.21.5': - optional: true - '@esbuild/sunos-x64@0.25.10': optional: true - '@esbuild/win32-arm64@0.21.5': - optional: true - '@esbuild/win32-arm64@0.25.10': optional: true - '@esbuild/win32-ia32@0.21.5': - optional: true - '@esbuild/win32-ia32@0.25.10': optional: true - '@esbuild/win32-x64@0.21.5': - optional: true - '@esbuild/win32-x64@0.25.10': optional: true @@ -2463,17 +3087,188 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + '@istanbuljs/schema@0.1.3': {} + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.19.18 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.18 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.18 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.19.18 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 20.19.18 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.28.4 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.19.18 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2483,6 +3278,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.5.0 @@ -2577,13 +3377,69 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.4 + '@types/estree@1.0.8': {} + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 20.19.18 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -2592,86 +3448,106 @@ snapshots: dependencies: undici-types: 6.21.0 - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2)': + '@types/stack-utils@2.0.3': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 7.18.0(eslint@9.36.0)(typescript@5.9.2) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.36.0)(typescript@5.9.2) - '@typescript-eslint/utils': 7.18.0(eslint@9.36.0)(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 7.18.0 + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/type-utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.45.0 eslint: 9.36.0 graphemer: 1.4.0 - ignore: 5.3.2 + ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 1.4.3(typescript@5.9.2) - optionalDependencies: + ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2)': + '@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2)': dependencies: - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 7.18.0 + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.45.0 debug: 4.4.3 eslint: 9.36.0 - optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@7.18.0': + '@typescript-eslint/project-service@8.45.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 + '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.2) + '@typescript-eslint/types': 8.45.0 + debug: 4.4.3 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color - '@typescript-eslint/type-utils@7.18.0(eslint@9.36.0)(typescript@5.9.2)': + '@typescript-eslint/scope-manager@8.45.0': dependencies: - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) - '@typescript-eslint/utils': 7.18.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/visitor-keys': 8.45.0 + + '@typescript-eslint/tsconfig-utils@8.45.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@typescript-eslint/type-utils@8.45.0(eslint@9.36.0)(typescript@5.9.2)': + dependencies: + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2) debug: 4.4.3 eslint: 9.36.0 - ts-api-utils: 1.4.3(typescript@5.9.2) - optionalDependencies: + ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@7.18.0': {} + '@typescript-eslint/types@8.45.0': {} - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.2)': + '@typescript-eslint/typescript-estree@8.45.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 + '@typescript-eslint/project-service': 8.45.0(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.2) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/visitor-keys': 8.45.0 debug: 4.4.3 - globby: 11.1.0 + fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.2 - ts-api-utils: 1.4.3(typescript@5.9.2) - optionalDependencies: + ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.18.0(eslint@9.36.0)(typescript@5.9.2)': + '@typescript-eslint/utils@8.45.0(eslint@9.36.0)(typescript@5.9.2)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.2) eslint: 9.36.0 + typescript: 5.9.2 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/visitor-keys@7.18.0': + '@typescript-eslint/visitor-keys@8.45.0': dependencies: - '@typescript-eslint/types': 7.18.0 - eslint-visitor-keys: 3.4.3 + '@typescript-eslint/types': 8.45.0 + eslint-visitor-keys: 4.2.1 '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -2732,54 +3608,6 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitest/coverage-v8@1.6.1(vitest@1.6.1(@types/node@20.19.18))': - dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 0.2.3 - debug: 4.4.3 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.2.0 - magic-string: 0.30.19 - magicast: 0.3.5 - picocolors: 1.1.1 - std-env: 3.9.0 - strip-literal: 2.1.1 - test-exclude: 6.0.0 - vitest: 1.6.1(@types/node@20.19.18) - transitivePeerDependencies: - - supports-color - - '@vitest/expect@1.6.1': - dependencies: - '@vitest/spy': 1.6.1 - '@vitest/utils': 1.6.1 - chai: 4.5.0 - - '@vitest/runner@1.6.1': - dependencies: - '@vitest/utils': 1.6.1 - p-limit: 5.0.0 - pathe: 1.1.2 - - '@vitest/snapshot@1.6.1': - dependencies: - magic-string: 0.30.19 - pathe: 1.1.2 - pretty-format: 29.7.0 - - '@vitest/spy@1.6.1': - dependencies: - tinyspy: 2.2.1 - - '@vitest/utils@1.6.1': - dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -2797,6 +3625,10 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -2811,6 +3643,17 @@ snapshots: any-promise@1.3.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} array-buffer-byte-length@1.0.2: @@ -2829,8 +3672,6 @@ snapshots: is-string: 1.1.1 math-intrinsics: 1.1.0 - array-union@2.1.0: {} - array.prototype.findlastindex@1.2.6: dependencies: call-bind: 1.0.8 @@ -2865,16 +3706,71 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 - assertion-error@1.1.0: {} - async-function@1.0.0: {} available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 + babel-jest@29.7.0(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.28.4) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.4) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.4) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.4) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.4) + + babel-preset-jest@29.6.3(@babel/core@7.28.4): + dependencies: + '@babel/core': 7.28.4 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) + balanced-match@1.0.2: {} + baseline-browser-mapping@2.8.9: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -2888,6 +3784,24 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.26.2: + dependencies: + baseline-browser-mapping: 2.8.9 + caniuse-lite: 1.0.30001745 + electron-to-chromium: 1.5.227 + node-releases: 2.0.21 + update-browserslist-db: 1.1.3(browserslist@4.26.2) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} + bundle-require@5.1.0(esbuild@0.25.10): dependencies: esbuild: 0.25.10 @@ -2914,29 +3828,37 @@ snapshots: callsites@3.1.0: {} - chai@4.5.0: - dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.4 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.1.0 + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001745: {} chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - check-error@1.0.3: - dependencies: - get-func-name: 2.0.2 + char-regex@1.0.2: {} chokidar@4.0.3: dependencies: readdirp: 4.1.2 + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.3: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + co@4.6.0: {} + + collect-v8-coverage@1.0.2: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2951,6 +3873,25 @@ snapshots: consola@3.4.2: {} + convert-source-map@2.0.0: {} + + create-jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + create-require@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2983,12 +3924,12 @@ snapshots: dependencies: ms: 2.1.3 - deep-eql@4.1.4: - dependencies: - type-detect: 4.1.0 + dedent@1.7.0: {} deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -3001,11 +3942,11 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + detect-newline@3.1.0: {} + diff-sequences@29.6.3: {} - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 + diff@4.0.2: {} doctrine@2.1.0: dependencies: @@ -3019,10 +3960,18 @@ snapshots: eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.227: {} + + emittery@0.13.1: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 @@ -3105,32 +4054,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - esbuild@0.25.10: optionalDependencies: '@esbuild/aix-ppc64': 0.25.10 @@ -3160,6 +4083,10 @@ snapshots: '@esbuild/win32-ia32': 0.25.10 '@esbuild/win32-x64': 0.25.10 + escalade@3.2.0: {} + + escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} eslint-config-prettier@9.1.2(eslint@9.36.0): @@ -3185,22 +4112,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0)(typescript@5.9.2) eslint: 9.36.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -3211,7 +4138,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.36.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -3223,7 +4150,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0)(typescript@5.9.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -3284,6 +4211,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 4.2.1 + esprima@4.0.1: {} + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -3294,23 +4223,29 @@ snapshots: estraverse@5.3.0: {} - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - esutils@2.0.3: {} - execa@8.0.1: + execa@5.1.1: dependencies: cross-spawn: 7.0.6 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit@0.1.2: {} + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 fast-deep-equal@3.1.3: {} @@ -3330,6 +4265,10 @@ snapshots: dependencies: reusify: 1.1.0 + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -3342,6 +4281,11 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -3389,7 +4333,9 @@ snapshots: generator-function@2.0.0: {} - get-func-name@2.0.2: {} + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} get-intrinsic@1.3.0: dependencies: @@ -3404,12 +4350,14 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-package-type@0.1.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@8.0.1: {} + get-stream@6.0.1: {} get-symbol-description@1.1.0: dependencies: @@ -3454,19 +4402,21 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - gopd@1.2.0: {} + graceful-fs@4.2.11: {} + graphemer@1.4.0: {} + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -3491,15 +4441,22 @@ snapshots: html-escaper@2.0.2: {} - human-signals@5.0.0: {} + human-signals@2.1.0: {} ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + imurmurhash@0.1.4: {} inflight@1.0.6: @@ -3521,6 +4478,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -3567,6 +4526,8 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-generator-fn@2.1.0: {} + is-generator-function@1.1.1: dependencies: call-bound: 1.0.4 @@ -3603,7 +4564,7 @@ snapshots: dependencies: call-bound: 1.0.4 - is-stream@3.0.0: {} + is-stream@2.0.1: {} is-string@1.1.1: dependencies: @@ -3637,17 +4598,37 @@ snapshots: istanbul-lib-coverage@3.2.2: {} + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.6: + istanbul-lib-source-maps@4.0.1: dependencies: - '@jridgewell/trace-mapping': 0.3.31 debug: 4.4.3 istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 transitivePeerDependencies: - supports-color @@ -3662,16 +4643,334 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.18 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.0 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)): + dependencies: + '@babel/core': 7.28.4 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.4) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.19.18 + ts-node: 10.9.2(@types/node@20.19.18)(typescript@5.9.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.18 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 20.19.18 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.19.18 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.10 + resolve.exports: 2.0.3 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.18 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.18 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.28.4 + '@babel/generator': 7.28.3 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) + '@babel/types': 7.28.4 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.19.18 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.18 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@29.7.0: + dependencies: + '@types/node': 20.19.18 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + joycon@3.1.1: {} - js-tokens@9.0.1: {} + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 js-yaml@4.1.0: dependencies: argparse: 2.0.1 + jsesc@3.1.0: {} + json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -3680,10 +4979,16 @@ snapshots: dependencies: minimist: 1.2.8 + json5@2.2.3: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 + kleur@3.0.3: {} + + leven@3.1.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -3695,39 +5000,40 @@ snapshots: load-tsconfig@0.2.5: {} - local-pkg@0.5.1: + locate-path@5.0.0: dependencies: - mlly: 1.8.0 - pkg-types: 1.3.1 + p-locate: 4.1.0 locate-path@6.0.0: dependencies: p-locate: 5.0.0 + lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} lodash.sortby@4.7.0: {} - loupe@2.3.7: - dependencies: - get-func-name: 2.0.2 - lru-cache@10.4.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - magicast@0.3.5: - dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - source-map-js: 1.2.1 - make-dir@4.0.0: dependencies: semver: 7.7.2 + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + math-intrinsics@1.1.0: {} merge-stream@2.0.0: {} @@ -3739,7 +5045,7 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mimic-fn@4.0.0: {} + mimic-fn@2.1.0: {} minimatch@3.1.2: dependencies: @@ -3768,15 +5074,24 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.11: {} + nanoid@3.3.11: + optional: true napi-postinstall@0.3.3: {} natural-compare@1.4.0: {} - npm-run-path@5.3.0: + neo-async@2.6.2: {} + + node-int64@0.4.0: {} + + node-releases@2.0.21: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: dependencies: - path-key: 4.0.0 + path-key: 3.1.1 object-assign@4.1.1: {} @@ -3817,9 +5132,9 @@ snapshots: dependencies: wrappy: 1.0.2 - onetime@6.0.0: + onetime@5.1.2: dependencies: - mimic-fn: 4.0.0 + mimic-fn: 2.1.0 optionator@0.9.4: dependencies: @@ -3836,32 +5151,43 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - p-limit@5.0.0: + p-locate@4.1.0: dependencies: - yocto-queue: 1.2.1 + p-limit: 2.3.0 p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} path-key@3.1.1: {} - path-key@4.0.0: {} - path-parse@1.0.7: {} path-scurry@1.11.1: @@ -3869,14 +5195,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-type@4.0.0: {} - - pathe@1.1.2: {} - pathe@2.0.3: {} - pathval@1.1.1: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3885,6 +5205,10 @@ snapshots: pirates@4.0.7: {} + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -3905,6 +5229,7 @@ snapshots: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + optional: true prelude-ls@1.2.1: {} @@ -3918,8 +5243,15 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + punycode@2.3.1: {} + pure-rand@6.1.0: {} + queue-microtask@1.2.3: {} react-is@18.3.1: {} @@ -3946,12 +5278,20 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + require-directory@2.1.1: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + resolve-from@4.0.0: {} resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} + resolve.exports@2.0.3: {} + resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -4069,29 +5409,46 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} + sisteransi@1.0.5: {} + slash@3.0.0: {} - source-map-js@1.2.1: {} + source-map-js@1.2.1: + optional: true + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} source-map@0.8.0-beta.0: dependencies: whatwg-url: 7.1.0 + sprintf-js@1.0.3: {} + stable-hash@0.0.5: {} - stackback@0.0.2: {} - - std-env@3.9.0: {} + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 internal-slot: 1.1.0 + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -4137,14 +5494,12 @@ snapshots: strip-bom@3.0.0: {} - strip-final-newline@3.0.0: {} + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} strip-json-comments@3.1.1: {} - strip-literal@2.1.1: - dependencies: - js-tokens: 9.0.1 - sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -4159,6 +5514,10 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} test-exclude@6.0.0: @@ -4175,8 +5534,6 @@ snapshots: dependencies: any-promise: 1.3.0 - tinybench@2.9.0: {} - tinyexec@0.3.2: {} tinyglobby@0.2.15: @@ -4184,9 +5541,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinypool@0.8.4: {} - - tinyspy@2.2.1: {} + tmpl@1.0.5: {} to-regex-range@5.0.1: dependencies: @@ -4198,12 +5553,51 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@1.4.3(typescript@5.9.2): + ts-api-utils@2.1.0(typescript@5.9.2): dependencies: typescript: 5.9.2 ts-interface-checker@0.1.13: {} + ts-jest@29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.2 + type-fest: 4.41.0 + typescript: 5.9.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.4 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.4) + esbuild: 0.25.10 + jest-util: 29.7.0 + + ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.19.18 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.9.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -4246,7 +5640,11 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-detect@4.1.0: {} + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} typed-array-buffer@1.0.3: dependencies: @@ -4285,6 +5683,9 @@ snapshots: ufo@1.6.1: {} + uglify-js@3.19.3: + optional: true + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -4318,70 +5719,27 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + update-browserslist-db@1.1.3(browserslist@4.26.2): + dependencies: + browserslist: 4.26.2 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 - vite-node@1.6.1(@types/node@20.19.18): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - pathe: 1.1.2 - picocolors: 1.1.1 - vite: 5.4.20(@types/node@20.19.18) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser + v8-compile-cache-lib@3.0.1: {} - vite@5.4.20(@types/node@20.19.18): + v8-to-istanbul@9.3.0: dependencies: - esbuild: 0.21.5 - postcss: 8.5.6 - rollup: 4.52.3 - optionalDependencies: - '@types/node': 20.19.18 - fsevents: 2.3.3 + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 - vitest@1.6.1(@types/node@20.19.18): + walker@1.0.8: dependencies: - '@vitest/expect': 1.6.1 - '@vitest/runner': 1.6.1 - '@vitest/snapshot': 1.6.1 - '@vitest/spy': 1.6.1 - '@vitest/utils': 1.6.1 - acorn-walk: 8.3.4 - chai: 4.5.0 - debug: 4.4.3 - execa: 8.0.1 - local-pkg: 0.5.1 - magic-string: 0.30.19 - pathe: 1.1.2 - picocolors: 1.1.1 - std-env: 3.9.0 - strip-literal: 2.1.1 - tinybench: 2.9.0 - tinypool: 0.8.4 - vite: 5.4.20(@types/node@20.19.18) - vite-node: 1.6.1(@types/node@20.19.18) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 20.19.18 - transitivePeerDependencies: - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser + makeerror: 1.0.12 webidl-conversions@4.0.2: {} @@ -4436,13 +5794,10 @@ snapshots: dependencies: isexe: 2.0.0 - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -4457,9 +5812,30 @@ snapshots: wrappy@1.0.2: {} + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + yaml@2.8.1: optional: true - yocto-queue@0.1.0: {} + yargs-parser@21.1.1: {} - yocto-queue@1.2.1: {} + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/sdk/typescript/.eslintrc.json b/sdk/typescript/.eslintrc.json index 5a3226c2be..1cae2c818d 100644 --- a/sdk/typescript/.eslintrc.json +++ b/sdk/typescript/.eslintrc.json @@ -35,6 +35,7 @@ { "ts-expect-error": "allow-with-description" } - ] + ], + "func-style": ["error", "declaration"] } } diff --git a/sdk/typescript/.prettierrc b/sdk/typescript/.prettierrc index 5e2863a11f..b68d0e11f5 100644 --- a/sdk/typescript/.prettierrc +++ b/sdk/typescript/.prettierrc @@ -1,5 +1,5 @@ { "printWidth": 100, - "singleQuote": true, + "singleQuote": false, "trailingComma": "all" } diff --git a/sdk/typescript/jest.config.cjs b/sdk/typescript/jest.config.cjs new file mode 100644 index 0000000000..55d25cd842 --- /dev/null +++ b/sdk/typescript/jest.config.cjs @@ -0,0 +1,16 @@ +/** @type {import('jest').Config} */ +module.exports = { + preset: "ts-jest/presets/default-esm", + testEnvironment: "node", + extensionsToTreatAsEsm: [".ts"], + globals: { + "ts-jest": { + useESM: true, + tsconfig: "tsconfig.json", + }, + }, + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + testMatch: ["**/tests/**/*.test.ts"], +}; diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index c74c34f9fe..a09de87c96 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -31,26 +31,28 @@ "build": "tsup", "build:watch": "tsup --watch", "lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\"", - "lint:fix": "pnpm run lint -- --fix", - "test": "vitest run", - "test:watch": "vitest watch", - "coverage": "vitest run --coverage", + "lint:fix": "eslint --fix \"src/**/*.ts\" \"tests/**/*.ts\"", + "test": "jest", + "test:watch": "jest --watch", + "coverage": "jest --coverage", "format": "prettier --check .", "format:fix": "prettier --write .", "prepare": "pnpm run build" }, "devDependencies": { + "@types/jest": "^29.5.14", "@types/node": "^20.19.18", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", - "@vitest/coverage-v8": "^1.6.1", + "@typescript-eslint/eslint-plugin": "^8.45.0", + "@typescript-eslint/parser": "^8.45.0", "eslint": "^9.36.0", "eslint-config-prettier": "^9.1.2", "eslint-import-resolver-typescript": "^3.10.1", "eslint-plugin-import": "^2.32.0", + "jest": "^29.7.0", "prettier": "^3.6.2", + "ts-jest": "^29.3.4", + "ts-node": "^10.9.2", "tsup": "^8.5.0", - "typescript": "^5.9.2", - "vitest": "^1.6.1" + "typescript": "^5.9.2" } } diff --git a/sdk/typescript/samples/basic_streaming.ts b/sdk/typescript/samples/basic_streaming.ts new file mode 100755 index 0000000000..41499b3e84 --- /dev/null +++ b/sdk/typescript/samples/basic_streaming.ts @@ -0,0 +1,91 @@ +#!/usr/bin/env -S NODE_NO_WARNINGS=1 pnpm ts-node-esm --files + +import { createInterface } from "node:readline/promises"; +import { stdin as input, stdout as output } from "node:process"; + +import { Codex } from "@openai/codex-sdk"; +import type { ConversationEvent, ConversationItem } from "@openai/codex-sdk"; +import path from "node:path"; + +const executablePath = + process.env.CODEX_EXECUTABLE ?? + path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex"); + +const codex = new Codex({ executablePath }); +const thread = codex.startThread(); +const rl = createInterface({ input, output }); + +const handleItemCompleted = (item: ConversationItem): void => { + switch (item.item_type) { + case "assistant_message": + console.log(`Assistant: ${item.text}`); + break; + case "reasoning": + console.log(`Reasoning: ${item.text}`); + break; + case "command_execution": { + const exitText = item.exit_code !== undefined ? ` Exit code ${item.exit_code}.` : ""; + console.log(`Command ${item.command} ${item.status}.${exitText}`); + break; + } + case "file_change": { + for (const change of item.changes) { + console.log(`File ${change.kind} ${change.path}`); + } + break; + } + } +}; + +const handleItemUpdated = (item: ConversationItem): void => { + switch (item.item_type) { + case "todo_list": { + console.log(`Todo:`); + for (const todo of item.items) { + console.log(`\t ${todo.completed ? "x" : " "} ${todo.text}`); + } + break; + } + } +}; + +const handleEvent = (event: ConversationEvent): void => { + switch (event.type) { + case "item.completed": + handleItemCompleted(event.item); + break; + case "item.updated": + case "item.started": + handleItemUpdated(event.item); + break; + case "turn.completed": + console.log( + `Used ${event.usage.input_tokens} input tokens, ${event.usage.cached_input_tokens} cached input tokens, ${event.usage.output_tokens} output tokens.`, + ); + break; + } +}; + +const main = async (): Promise => { + try { + while (true) { + const inputText = await rl.question(">"); + const trimmed = inputText.trim(); + if (trimmed.length === 0) { + continue; + } + const { events } = await thread.runStreamed(inputText); + for await (const event of events) { + handleEvent(event); + } + } + } finally { + rl.close(); + } +}; + +main().catch((err) => { + const message = err instanceof Error ? err.message : String(err); + console.error(`Unexpected error: ${message}`); + process.exit(1); +}); diff --git a/sdk/typescript/src/codex.ts b/sdk/typescript/src/codex.ts new file mode 100644 index 0000000000..36275bc7d0 --- /dev/null +++ b/sdk/typescript/src/codex.ts @@ -0,0 +1,25 @@ +import { CodexOptions } from "./codexOptions"; +import { CodexExec } from "./exec"; +import { Thread } from "./thread"; + +export class Codex { + private exec: CodexExec; + private options: CodexOptions; + + constructor(options: CodexOptions) { + if (!options.executablePath) { + throw new Error("executablePath is required"); + } + + this.exec = new CodexExec(options.executablePath); + this.options = options; + } + + startThread(): Thread { + return new Thread(this.exec, this.options); + } + + resumeThread(id: string): Thread { + return new Thread(this.exec, this.options, id); + } +} diff --git a/sdk/typescript/src/codexOptions.ts b/sdk/typescript/src/codexOptions.ts new file mode 100644 index 0000000000..16113bc2de --- /dev/null +++ b/sdk/typescript/src/codexOptions.ts @@ -0,0 +1,7 @@ +export type CodexOptions = { + // TODO: remove + executablePath: string; + // TODO: remove + baseUrl?: string; + apiKey?: string; +}; diff --git a/sdk/typescript/src/events.ts b/sdk/typescript/src/events.ts new file mode 100644 index 0000000000..89402bc75e --- /dev/null +++ b/sdk/typescript/src/events.ts @@ -0,0 +1,52 @@ +// based on event types from codex-rs/exec/src/exec_events.rs + +import type { ConversationItem } from "./items"; + +export type SessionCreatedEvent = { + type: "session.created"; + session_id: string; +}; + +export type TurnStartedEvent = { + type: "turn.started"; +}; + +export type Usage = { + input_tokens: number; + cached_input_tokens: number; + output_tokens: number; +}; + +export type TurnCompletedEvent = { + type: "turn.completed"; + usage: Usage; +}; + +export type ItemStartedEvent = { + type: "item.started"; + item: ConversationItem; +}; + +export type ItemUpdatedEvent = { + type: "item.updated"; + item: ConversationItem; +}; + +export type ItemCompletedEvent = { + type: "item.completed"; + item: ConversationItem; +}; + +export type ConversationErrorEvent = { + type: "error"; + message: string; +}; + +export type ConversationEvent = + | SessionCreatedEvent + | TurnStartedEvent + | TurnCompletedEvent + | ItemStartedEvent + | ItemUpdatedEvent + | ItemCompletedEvent + | ConversationErrorEvent; diff --git a/sdk/typescript/src/exec.ts b/sdk/typescript/src/exec.ts new file mode 100644 index 0000000000..9a078aa9c6 --- /dev/null +++ b/sdk/typescript/src/exec.ts @@ -0,0 +1,81 @@ +import { spawn } from "child_process"; +import readline from "node:readline"; + +export type CodexExecArgs = { + input: string; + + baseUrl?: string; + apiKey?: string; + sessionId?: string | null; +}; + +export class CodexExec { + private executablePath: string; + constructor(executablePath: string) { + this.executablePath = executablePath; + } + + async *run(args: CodexExecArgs): AsyncGenerator { + const commandArgs: string[] = ["exec", "--experimental-json"]; + if (args.sessionId) { + commandArgs.push("resume", args.sessionId, args.input); + } else { + commandArgs.push(args.input); + } + + const env = { + ...process.env, + }; + if (args.baseUrl) { + env.OPENAI_BASE_URL = args.baseUrl; + } + if (args.apiKey) { + env.OPENAI_API_KEY = args.apiKey; + } + + const child = spawn(this.executablePath, commandArgs, { + env, + }); + + let spawnError: unknown | null = null; + child.once("error", (err) => (spawnError = err)); + + if (!child.stdout) { + child.kill(); + throw new Error("Child process has no stdout"); + } + + const rl = readline.createInterface({ + input: child.stdout, + crlfDelay: Infinity, + }); + + try { + for await (const line of rl) { + // `line` is a string (Node sets default encoding to utf8 for readline) + yield line as string; + } + + const exitCode = new Promise((resolve) => { + child.once("exit", (code) => { + if (code === 0) { + resolve(code); + } else { + throw new Error(`Codex Exec exited with code ${code}`); + } + }); + }); + + if (spawnError) throw spawnError; + await exitCode; + } finally { + rl.close(); + child.removeAllListeners(); + try { + if (!child.killed) child.kill(); + } catch { + // ignore + } + } + } +} diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 40d4b9b2d0..f46471fd04 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -1,4 +1,27 @@ -export class Codex { - constructor() { - } -} +export type { + ConversationEvent, + SessionCreatedEvent, + TurnStartedEvent, + TurnCompletedEvent, + ItemStartedEvent, + ItemUpdatedEvent, + ItemCompletedEvent, + ConversationErrorEvent, +} from "./events"; +export type { + ConversationItem, + AssistantMessageItem, + ReasoningItem, + CommandExecutionItem, + FileChangeItem, + McpToolCallItem, + WebSearchItem, + TodoListItem, + ErrorItem, +} from "./items"; + +export type { Thread, RunResult, RunStreamedResult, Input } from "./thread"; + +export type { Codex } from "./codex"; + +export type { CodexOptions } from "./codexOptions"; diff --git a/sdk/typescript/src/items.ts b/sdk/typescript/src/items.ts new file mode 100644 index 0000000000..82fb0bb93c --- /dev/null +++ b/sdk/typescript/src/items.ts @@ -0,0 +1,99 @@ +// based on item types from codex-rs/exec/src/exec_events.rs + +export type CommandExecutionStatus = "in_progress" | "completed" | "failed"; + +export type CommandExecutionItem = { + id: string; + item_type: "command_execution"; + command: string; + aggregated_output: string; + exit_code?: number; + status: CommandExecutionStatus; +}; + +export type PatchChangeKind = "add" | "delete" | "update"; + +export type FileUpdateChange = { + path: string; + kind: PatchChangeKind; +}; + +export type PatchApplyStatus = "completed" | "failed"; + +export type FileChangeItem = { + id: string; + item_type: "file_change"; + changes: FileUpdateChange[]; + status: PatchApplyStatus; +}; + +export type McpToolCallStatus = "in_progress" | "completed" | "failed"; + +export type McpToolCallItem = { + id: string; + item_type: "mcp_tool_call"; + server: string; + tool: string; + status: McpToolCallStatus; +}; + +export type AssistantMessageItem = { + id: string; + item_type: "assistant_message"; + text: string; +}; + +export type ReasoningItem = { + id: string; + item_type: "reasoning"; + text: string; +}; + +export type WebSearchItem = { + id: string; + item_type: "web_search"; + query: string; +}; + +export type ErrorItem = { + id: string; + item_type: "error"; + message: string; +}; + +export type TodoItem = { + text: string; + completed: boolean; +}; + +export type TodoListItem = { + id: string; + item_type: "todo_list"; + items: TodoItem[]; +}; + +export type SessionItem = { + id: string; + item_type: "session"; + session_id: string; +}; + +export type ConversationItem = + | AssistantMessageItem + | ReasoningItem + | CommandExecutionItem + | FileChangeItem + | McpToolCallItem + | WebSearchItem + | TodoListItem + | ErrorItem; + +export type ConversationItemDetails = + | AssistantMessageItem + | ReasoningItem + | CommandExecutionItem + | FileChangeItem + | McpToolCallItem + | WebSearchItem + | TodoListItem + | ErrorItem; diff --git a/sdk/typescript/src/thread.ts b/sdk/typescript/src/thread.ts new file mode 100644 index 0000000000..2091f6774f --- /dev/null +++ b/sdk/typescript/src/thread.ts @@ -0,0 +1,62 @@ +import { CodexOptions } from "./codexOptions"; +import { ConversationEvent } from "./events"; +import { CodexExec } from "./exec"; +import { ConversationItem } from "./items"; + +export type RunResult = { + items: ConversationItem[]; + finalResponse: string; +}; + +export type RunStreamedResult = { + events: AsyncGenerator; +}; + +export type Input = string; + +export class Thread { + private exec: CodexExec; + private options: CodexOptions; + public id: string | null; + + constructor(exec: CodexExec, options: CodexOptions, id: string | null = null) { + this.exec = exec; + this.options = options; + this.id = id; + } + + async runStreamed(input: string): Promise { + return { events: this.runStreamedInternal(input) }; + } + + private async *runStreamedInternal(input: string): AsyncGenerator { + const generator = this.exec.run({ + input, + baseUrl: this.options.baseUrl, + apiKey: this.options.apiKey, + sessionId: this.id, + }); + for await (const item of generator) { + const parsed = JSON.parse(item) as ConversationEvent; + if (parsed.type === "session.created") { + this.id = parsed.session_id; + } + yield parsed; + } + } + + async run(input: string): Promise { + const generator = this.runStreamedInternal(input); + const items: ConversationItem[] = []; + let finalResponse: string = ""; + for await (const event of generator) { + if (event.type === "item.completed") { + if (event.item.item_type === "assistant_message") { + finalResponse = event.item.text; + } + items.push(event.item); + } + } + return { items, finalResponse }; + } +} diff --git a/sdk/typescript/tests/index.test.ts b/sdk/typescript/tests/index.test.ts deleted file mode 100644 index c025118be2..0000000000 --- a/sdk/typescript/tests/index.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { Codex } from "../src/index.js"; - -describe("Codex", () => { - it("exposes the placeholder API", () => { - const client = new Codex(); - }); -}); diff --git a/sdk/typescript/tests/responsesProxy.ts b/sdk/typescript/tests/responsesProxy.ts new file mode 100644 index 0000000000..5c7815b538 --- /dev/null +++ b/sdk/typescript/tests/responsesProxy.ts @@ -0,0 +1,173 @@ +import http from "node:http"; + +const DEFAULT_RESPONSE_ID = "resp_mock"; +const DEFAULT_MESSAGE_ID = "msg_mock"; + +type SseEvent = { + type: string; + [key: string]: unknown; +}; + +type SseResponseBody = { + kind: "sse"; + events: SseEvent[]; +}; + +export type ResponsesProxyOptions = { + responseBodies: SseResponseBody[]; + statusCode?: number; +}; + +export type ResponsesProxy = { + url: string; + close: () => Promise; + requests: RecordedRequest[]; +}; + +export type ResponsesApiRequest = { + input: Array<{ + role: string; + content?: Array<{ type: string; text: string }>; + }>; +}; + +export type RecordedRequest = { + body: string; + json: ResponsesApiRequest; +}; + +function formatSseEvent(event: SseEvent): string { + return `event: ${event.type}\n` + `data: ${JSON.stringify(event)}\n\n`; +} + +export async function startResponsesTestProxy( + options: ResponsesProxyOptions, +): Promise { + const responseBodies = options.responseBodies; + if (responseBodies.length === 0) { + throw new Error("responseBodies is required"); + } + + const requests: RecordedRequest[] = []; + + function readRequestBody(req: http.IncomingMessage): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + req.on("data", (chunk) => { + chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); + }); + req.on("end", () => { + resolve(Buffer.concat(chunks).toString("utf8")); + }); + req.on("error", reject); + }); + } + + let responseIndex = 0; + + const server = http.createServer((req, res) => { + async function handle(): Promise { + if (req.method === "POST" && req.url === "/responses") { + const body = await readRequestBody(req); + const json = JSON.parse(body); + requests.push({ body, json }); + + const status = options.statusCode ?? 200; + res.statusCode = status; + res.setHeader("content-type", "text/event-stream"); + + const responseBody = responseBodies[Math.min(responseIndex, responseBodies.length - 1)]!; + responseIndex += 1; + for (const event of responseBody.events) { + res.write(formatSseEvent(event)); + } + res.end(); + return; + } + + res.statusCode = 404; + res.end(); + } + + handle().catch(() => { + res.statusCode = 500; + res.end(); + }); + }); + + const url = await new Promise((resolve, reject) => { + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (!address || typeof address === "string") { + reject(new Error("Unable to determine proxy address")); + return; + } + server.off("error", reject); + const info = address; + resolve(`http://${info.address}:${info.port}`); + }); + server.once("error", reject); + }); + + async function close(): Promise { + await new Promise((resolve, reject) => { + server.close((err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); + } + return { url, close, requests }; +} + +export function sse(...events: SseEvent[]): SseResponseBody { + return { + kind: "sse", + events, + }; +} + +export function responseStarted(responseId: string = DEFAULT_RESPONSE_ID): SseEvent { + return { + type: "response.created", + response: { + id: responseId, + }, + }; +} + +export function assistantMessage(text: string, itemId: string = DEFAULT_MESSAGE_ID): SseEvent { + return { + type: "response.output_item.done", + item: { + type: "message", + role: "assistant", + id: itemId, + content: [ + { + type: "output_text", + text, + }, + ], + }, + }; +} + +export function responseCompleted(responseId: string = DEFAULT_RESPONSE_ID): SseEvent { + return { + type: "response.completed", + response: { + id: responseId, + usage: { + input_tokens: 0, + input_tokens_details: null, + output_tokens: 0, + output_tokens_details: null, + total_tokens: 0, + }, + }, + }; +} diff --git a/sdk/typescript/tests/run.test.ts b/sdk/typescript/tests/run.test.ts new file mode 100644 index 0000000000..9f7fb792b9 --- /dev/null +++ b/sdk/typescript/tests/run.test.ts @@ -0,0 +1,133 @@ +import path from "path"; + +import { describe, expect, it } from "@jest/globals"; + +import { Codex } from "../src/codex"; + +import { + assistantMessage, + responseCompleted, + responseStarted, + sse, + startResponsesTestProxy, +} from "./responsesProxy"; + +const codexExecPath = path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex"); + +describe("Codex", () => { + it("returns session events", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [sse(responseStarted(), assistantMessage("Hi!"), responseCompleted())], + }); + + try { + const client = new Codex({ executablePath: codexExecPath, baseUrl: url, apiKey: "test" }); + + const thread = client.startThread(); + const result = await thread.run("Hello, world!"); + + const expectedItems = [ + { + id: expect.any(String), + item_type: "assistant_message", + text: "Hi!", + }, + ]; + expect(result.items).toEqual(expectedItems); + expect(thread.id).toEqual(expect.any(String)); + } finally { + await close(); + } + }); + + it("sends previous items when run is called twice", async () => { + const { url, close, requests } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted("response_1"), + assistantMessage("First response", "item_1"), + responseCompleted("response_1"), + ), + sse( + responseStarted("response_2"), + assistantMessage("Second response", "item_2"), + responseCompleted("response_2"), + ), + ], + }); + + try { + const client = new Codex({ executablePath: codexExecPath, baseUrl: url, apiKey: "test" }); + + const thread = client.startThread(); + await thread.run("first input"); + await thread.run("second input"); + + // Check second request continues conversation + expect(requests.length).toBeGreaterThanOrEqual(2); + const secondRequest = requests[1]; + expect(secondRequest).toBeDefined(); + const payload = secondRequest!.json; + + const assistantEntry = payload.input.find( + (entry: { role: string }) => entry.role === "assistant", + ); + expect(assistantEntry).toBeDefined(); + const assistantText = assistantEntry?.content?.find( + (item: { type: string; text: string }) => item.type === "output_text", + )?.text; + expect(assistantText).toBe("First response"); + } finally { + await close(); + } + }); + + it("resumes thread by id", async () => { + const { url, close, requests } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted("response_1"), + assistantMessage("First response", "item_1"), + responseCompleted("response_1"), + ), + sse( + responseStarted("response_2"), + assistantMessage("Second response", "item_2"), + responseCompleted("response_2"), + ), + ], + }); + + try { + const client = new Codex({ executablePath: codexExecPath, baseUrl: url, apiKey: "test" }); + + const originalThread = client.startThread(); + await originalThread.run("first input"); + + const resumedThread = client.resumeThread(originalThread.id!); + const result = await resumedThread.run("second input"); + + expect(resumedThread.id).toBe(originalThread.id); + expect(result.finalResponse).toBe("Second response"); + + expect(requests.length).toBeGreaterThanOrEqual(2); + const secondRequest = requests[1]; + expect(secondRequest).toBeDefined(); + const payload = secondRequest!.json; + + const assistantEntry = payload.input.find( + (entry: { role: string }) => entry.role === "assistant", + ); + expect(assistantEntry).toBeDefined(); + const assistantText = assistantEntry?.content?.find( + (item: { type: string; text: string }) => item.type === "output_text", + )?.text; + expect(assistantText).toBe("First response"); + } finally { + await close(); + } + }); +}); diff --git a/sdk/typescript/tests/runStreamed.test.ts b/sdk/typescript/tests/runStreamed.test.ts new file mode 100644 index 0000000000..cb10435a86 --- /dev/null +++ b/sdk/typescript/tests/runStreamed.test.ts @@ -0,0 +1,167 @@ +import path from "path"; + +import { describe, expect, it } from "@jest/globals"; + +import { Codex } from "../src/codex"; +import { ConversationEvent } from "../src/index"; + +import { + assistantMessage, + responseCompleted, + responseStarted, + sse, + startResponsesTestProxy, +} from "./responsesProxy"; + +const codexExecPath = path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex"); + +describe("Codex", () => { + it("returns session events", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [sse(responseStarted(), assistantMessage("Hi!"), responseCompleted())], + }); + + try { + const client = new Codex({ executablePath: codexExecPath, baseUrl: url, apiKey: "test" }); + + const thread = client.startThread(); + const result = await thread.runStreamed("Hello, world!"); + + const events: ConversationEvent[] = []; + for await (const event of result.events) { + events.push(event); + } + + expect(events).toEqual([ + { + type: "session.created", + session_id: expect.any(String), + }, + { + type: "turn.started", + }, + { + type: "item.completed", + item: { + id: "item_0", + item_type: "assistant_message", + text: "Hi!", + }, + }, + { + type: "turn.completed", + usage: { + cached_input_tokens: 0, + input_tokens: 0, + output_tokens: 0, + }, + }, + ]); + expect(thread.id).toEqual(expect.any(String)); + } finally { + await close(); + } + }); + + it("sends previous items when runStreamed is called twice", async () => { + const { url, close, requests } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted("response_1"), + assistantMessage("First response", "item_1"), + responseCompleted("response_1"), + ), + sse( + responseStarted("response_2"), + assistantMessage("Second response", "item_2"), + responseCompleted("response_2"), + ), + ], + }); + + try { + const client = new Codex({ executablePath: codexExecPath, baseUrl: url, apiKey: "test" }); + + const thread = client.startThread(); + const first = await thread.runStreamed("first input"); + await drainEvents(first.events); + + const second = await thread.runStreamed("second input"); + await drainEvents(second.events); + + // Check second request continues conversation + expect(requests.length).toBeGreaterThanOrEqual(2); + const secondRequest = requests[1]; + expect(secondRequest).toBeDefined(); + const payload = secondRequest!.json; + + const assistantEntry = payload.input.find( + (entry: { role: string }) => entry.role === "assistant", + ); + expect(assistantEntry).toBeDefined(); + const assistantText = assistantEntry?.content?.find( + (item: { type: string; text: string }) => item.type === "output_text", + )?.text; + expect(assistantText).toBe("First response"); + } finally { + await close(); + } + }); + + it("resumes thread by id when streaming", async () => { + const { url, close, requests } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted("response_1"), + assistantMessage("First response", "item_1"), + responseCompleted("response_1"), + ), + sse( + responseStarted("response_2"), + assistantMessage("Second response", "item_2"), + responseCompleted("response_2"), + ), + ], + }); + + try { + const client = new Codex({ executablePath: codexExecPath, baseUrl: url, apiKey: "test" }); + + const originalThread = client.startThread(); + const first = await originalThread.runStreamed("first input"); + await drainEvents(first.events); + + const resumedThread = client.resumeThread(originalThread.id!); + const second = await resumedThread.runStreamed("second input"); + await drainEvents(second.events); + + expect(resumedThread.id).toBe(originalThread.id); + + expect(requests.length).toBeGreaterThanOrEqual(2); + const secondRequest = requests[1]; + expect(secondRequest).toBeDefined(); + const payload = secondRequest!.json; + + const assistantEntry = payload.input.find( + (entry: { role: string }) => entry.role === "assistant", + ); + expect(assistantEntry).toBeDefined(); + const assistantText = assistantEntry?.content?.find( + (item: { type: string; text: string }) => item.type === "output_text", + )?.text; + expect(assistantText).toBe("First response"); + } finally { + await close(); + } + }); +}); + +async function drainEvents(events: AsyncGenerator): Promise { + let done = false; + do { + done = (await events.next()).done ?? false; + } while (!done); +} diff --git a/sdk/typescript/tsconfig.json b/sdk/typescript/tsconfig.json index a57827583c..7dd88b00f4 100644 --- a/sdk/typescript/tsconfig.json +++ b/sdk/typescript/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "ESNext", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, @@ -11,12 +11,13 @@ "noUncheckedIndexedAccess": true, "resolveJsonModule": true, "lib": ["ES2022"], - "types": ["node", "vitest"], + "types": ["node", "jest"], "sourceMap": true, "declaration": true, "declarationMap": true, + "noImplicitAny": true, "outDir": "dist" }, - "include": ["src", "tests", "vitest.config.ts", "tsup.config.ts"], + "include": ["src", "tests", "tsup.config.ts"], "exclude": ["dist", "node_modules"] } diff --git a/sdk/typescript/vitest.config.ts b/sdk/typescript/vitest.config.ts deleted file mode 100644 index ad3095c2e9..0000000000 --- a/sdk/typescript/vitest.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - globals: true, - coverage: { - provider: "v8", - reporter: ["text", "lcov"], - }, - }, -}); From a8edc57740466a70545bc6658a69ed1de2a8ee07 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 29 Sep 2025 19:45:11 -0700 Subject: [PATCH 16/23] Add MCP tool call item to codex exec (#4481) No arguments/results for now. ``` { "type": "item.started", "item": { "id": "item_1", "item_type": "mcp_tool_call", "server": "github", "tool": "search_issues", "status": "in_progress" } } { "type": "item.completed", "item": { "id": "item_1", "item_type": "mcp_tool_call", "server": "github", "tool": "search_issues", "status": "completed" } } ``` --- codex-rs/Cargo.lock | 1 + codex-rs/exec/Cargo.toml | 1 + ...mental_event_processor_with_json_output.rs | 77 +++++++++++++ .../tests/event_processor_with_json_output.rs | 109 ++++++++++++++++++ 4 files changed, 188 insertions(+) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 0b811c3655..c8f362aa03 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -797,6 +797,7 @@ dependencies = [ "codex-protocol", "core_test_support", "libc", + "mcp-types", "opentelemetry-appender-tracing", "owo-colors", "predicates", diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index e37865b25c..27ae067c92 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -58,3 +58,4 @@ tempfile = { workspace = true } uuid = { workspace = true } walkdir = { workspace = true } wiremock = { workspace = true } +mcp-types = { workspace = true } diff --git a/codex-rs/exec/src/experimental_event_processor_with_json_output.rs b/codex-rs/exec/src/experimental_event_processor_with_json_output.rs index 7d4f12f755..89c10c3495 100644 --- a/codex-rs/exec/src/experimental_event_processor_with_json_output.rs +++ b/codex-rs/exec/src/experimental_event_processor_with_json_output.rs @@ -17,6 +17,8 @@ use crate::exec_events::FileUpdateChange; use crate::exec_events::ItemCompletedEvent; use crate::exec_events::ItemStartedEvent; use crate::exec_events::ItemUpdatedEvent; +use crate::exec_events::McpToolCallItem; +use crate::exec_events::McpToolCallStatus; use crate::exec_events::PatchApplyStatus; use crate::exec_events::PatchChangeKind; use crate::exec_events::ReasoningItem; @@ -37,6 +39,8 @@ use codex_core::protocol::EventMsg; use codex_core::protocol::ExecCommandBeginEvent; use codex_core::protocol::ExecCommandEndEvent; use codex_core::protocol::FileChange; +use codex_core::protocol::McpToolCallBeginEvent; +use codex_core::protocol::McpToolCallEndEvent; use codex_core::protocol::PatchApplyBeginEvent; use codex_core::protocol::PatchApplyEndEvent; use codex_core::protocol::SessionConfiguredEvent; @@ -54,6 +58,7 @@ pub struct ExperimentalEventProcessorWithJsonOutput { // Tracks the todo list for the current turn (at most one per turn). running_todo_list: Option, last_total_token_usage: Option, + running_mcp_tool_calls: HashMap, last_critical_error: Option, } @@ -69,6 +74,13 @@ struct RunningTodoList { items: Vec, } +#[derive(Debug, Clone)] +struct RunningMcpToolCall { + server: String, + tool: String, + item_id: String, +} + impl ExperimentalEventProcessorWithJsonOutput { pub fn new(last_message_path: Option) -> Self { Self { @@ -78,6 +90,7 @@ impl ExperimentalEventProcessorWithJsonOutput { running_patch_applies: HashMap::new(), running_todo_list: None, last_total_token_usage: None, + running_mcp_tool_calls: HashMap::new(), last_critical_error: None, } } @@ -89,6 +102,8 @@ impl ExperimentalEventProcessorWithJsonOutput { EventMsg::AgentReasoning(ev) => self.handle_reasoning_event(ev), EventMsg::ExecCommandBegin(ev) => self.handle_exec_command_begin(ev), EventMsg::ExecCommandEnd(ev) => self.handle_exec_command_end(ev), + EventMsg::McpToolCallBegin(ev) => self.handle_mcp_tool_call_begin(ev), + EventMsg::McpToolCallEnd(ev) => self.handle_mcp_tool_call_end(ev), EventMsg::PatchApplyBegin(ev) => self.handle_patch_apply_begin(ev), EventMsg::PatchApplyEnd(ev) => self.handle_patch_apply_end(ev), EventMsg::TokenCount(ev) => { @@ -193,6 +208,68 @@ impl ExperimentalEventProcessorWithJsonOutput { vec![ConversationEvent::ItemStarted(ItemStartedEvent { item })] } + fn handle_mcp_tool_call_begin(&mut self, ev: &McpToolCallBeginEvent) -> Vec { + let item_id = self.get_next_item_id(); + let server = ev.invocation.server.clone(); + let tool = ev.invocation.tool.clone(); + + self.running_mcp_tool_calls.insert( + ev.call_id.clone(), + RunningMcpToolCall { + server: server.clone(), + tool: tool.clone(), + item_id: item_id.clone(), + }, + ); + + let item = ConversationItem { + id: item_id, + details: ConversationItemDetails::McpToolCall(McpToolCallItem { + server, + tool, + status: McpToolCallStatus::InProgress, + }), + }; + + vec![ConversationEvent::ItemStarted(ItemStartedEvent { item })] + } + + fn handle_mcp_tool_call_end(&mut self, ev: &McpToolCallEndEvent) -> Vec { + let status = if ev.is_success() { + McpToolCallStatus::Completed + } else { + McpToolCallStatus::Failed + }; + + let (server, tool, item_id) = match self.running_mcp_tool_calls.remove(&ev.call_id) { + Some(running) => (running.server, running.tool, running.item_id), + None => { + warn!( + call_id = ev.call_id, + "Received McpToolCallEnd without begin; synthesizing new item" + ); + ( + ev.invocation.server.clone(), + ev.invocation.tool.clone(), + self.get_next_item_id(), + ) + } + }; + + let item = ConversationItem { + id: item_id, + details: ConversationItemDetails::McpToolCall(McpToolCallItem { + server, + tool, + status, + }), + }; + + vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { + item, + })] + } + fn handle_patch_apply_begin(&mut self, ev: &PatchApplyBeginEvent) -> Vec { self.running_patch_applies .insert(ev.call_id.clone(), ev.clone()); diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index f6f38d3254..198be44ad7 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -6,6 +6,9 @@ use codex_core::protocol::EventMsg; use codex_core::protocol::ExecCommandBeginEvent; use codex_core::protocol::ExecCommandEndEvent; use codex_core::protocol::FileChange; +use codex_core::protocol::McpInvocation; +use codex_core::protocol::McpToolCallBeginEvent; +use codex_core::protocol::McpToolCallEndEvent; use codex_core::protocol::PatchApplyBeginEvent; use codex_core::protocol::PatchApplyEndEvent; use codex_core::protocol::SessionConfiguredEvent; @@ -19,6 +22,8 @@ use codex_exec::exec_events::ConversationItemDetails; use codex_exec::exec_events::ItemCompletedEvent; use codex_exec::exec_events::ItemStartedEvent; use codex_exec::exec_events::ItemUpdatedEvent; +use codex_exec::exec_events::McpToolCallItem; +use codex_exec::exec_events::McpToolCallStatus; use codex_exec::exec_events::PatchApplyStatus; use codex_exec::exec_events::PatchChangeKind; use codex_exec::exec_events::ReasoningItem; @@ -30,6 +35,7 @@ use codex_exec::exec_events::TurnFailedEvent; use codex_exec::exec_events::TurnStartedEvent; use codex_exec::exec_events::Usage; use codex_exec::experimental_event_processor_with_json_output::ExperimentalEventProcessorWithJsonOutput; +use mcp_types::CallToolResult; use pretty_assertions::assert_eq; use std::path::PathBuf; use std::time::Duration; @@ -207,6 +213,109 @@ fn plan_update_emits_todo_list_started_updated_and_completed() { ); } +#[test] +fn mcp_tool_call_begin_and_end_emit_item_events() { + let mut ep = ExperimentalEventProcessorWithJsonOutput::new(None); + let invocation = McpInvocation { + server: "server_a".to_string(), + tool: "tool_x".to_string(), + arguments: None, + }; + + let begin = event( + "m1", + EventMsg::McpToolCallBegin(McpToolCallBeginEvent { + call_id: "call-1".to_string(), + invocation: invocation.clone(), + }), + ); + let begin_events = ep.collect_conversation_events(&begin); + assert_eq!( + begin_events, + vec![ConversationEvent::ItemStarted(ItemStartedEvent { + item: ConversationItem { + id: "item_0".to_string(), + details: ConversationItemDetails::McpToolCall(McpToolCallItem { + server: "server_a".to_string(), + tool: "tool_x".to_string(), + status: McpToolCallStatus::InProgress, + }), + }, + })] + ); + + let end = event( + "m2", + EventMsg::McpToolCallEnd(McpToolCallEndEvent { + call_id: "call-1".to_string(), + invocation, + duration: Duration::from_secs(1), + result: Ok(CallToolResult { + content: Vec::new(), + is_error: None, + structured_content: None, + }), + }), + ); + let end_events = ep.collect_conversation_events(&end); + assert_eq!( + end_events, + vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { + item: ConversationItem { + id: "item_0".to_string(), + details: ConversationItemDetails::McpToolCall(McpToolCallItem { + server: "server_a".to_string(), + tool: "tool_x".to_string(), + status: McpToolCallStatus::Completed, + }), + }, + })] + ); +} + +#[test] +fn mcp_tool_call_failure_sets_failed_status() { + let mut ep = ExperimentalEventProcessorWithJsonOutput::new(None); + let invocation = McpInvocation { + server: "server_b".to_string(), + tool: "tool_y".to_string(), + arguments: None, + }; + + let begin = event( + "m3", + EventMsg::McpToolCallBegin(McpToolCallBeginEvent { + call_id: "call-2".to_string(), + invocation: invocation.clone(), + }), + ); + ep.collect_conversation_events(&begin); + + let end = event( + "m4", + EventMsg::McpToolCallEnd(McpToolCallEndEvent { + call_id: "call-2".to_string(), + invocation, + duration: Duration::from_millis(5), + result: Err("tool exploded".to_string()), + }), + ); + let events = ep.collect_conversation_events(&end); + assert_eq!( + events, + vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { + item: ConversationItem { + id: "item_0".to_string(), + details: ConversationItemDetails::McpToolCall(McpToolCallItem { + server: "server_b".to_string(), + tool: "tool_y".to_string(), + status: McpToolCallStatus::Failed, + }), + }, + })] + ); +} + #[test] fn plan_update_after_complete_starts_new_todo_list_with_new_id() { use codex_core::plan_tool::PlanItemArg; From ea82f86662f17501e799ca8da55cf048c5b33d0e Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 29 Sep 2025 20:18:30 -0700 Subject: [PATCH 17/23] Rename conversation to thread in codex exec (#4482) --- codex-rs/exec/src/exec_events.rs | 28 +-- ...mental_event_processor_with_json_output.rs | 129 ++++++------- .../tests/event_processor_with_json_output.rs | 174 +++++++++--------- sdk/typescript/samples/basic_streaming.ts | 11 +- sdk/typescript/src/events.ts | 32 ++-- sdk/typescript/src/exec.ts | 6 +- sdk/typescript/src/index.ts | 10 +- sdk/typescript/src/items.ts | 12 +- sdk/typescript/src/thread.ts | 20 +- sdk/typescript/tests/run.test.ts | 4 +- sdk/typescript/tests/runStreamed.test.ts | 14 +- 11 files changed, 211 insertions(+), 229 deletions(-) diff --git a/codex-rs/exec/src/exec_events.rs b/codex-rs/exec/src/exec_events.rs index 0681111e81..f52071d4d3 100644 --- a/codex-rs/exec/src/exec_events.rs +++ b/codex-rs/exec/src/exec_events.rs @@ -2,10 +2,10 @@ use serde::Deserialize; use serde::Serialize; use ts_rs::TS; -/// Top-level events emitted on the Codex Exec conversation stream. +/// Top-level events emitted on the Codex Exec thread stream. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] #[serde(tag = "type")] -pub enum ConversationEvent { +pub enum ThreadEvent { #[serde(rename = "thread.started")] ThreadStarted(ThreadStartedEvent), #[serde(rename = "turn.started")] @@ -21,7 +21,7 @@ pub enum ConversationEvent { #[serde(rename = "item.completed")] ItemCompleted(ItemCompletedEvent), #[serde(rename = "error")] - Error(ConversationErrorEvent), + Error(ThreadErrorEvent), } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] @@ -39,7 +39,7 @@ pub struct TurnCompletedEvent { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] pub struct TurnFailedEvent { - pub error: ConversationErrorEvent, + pub error: ThreadErrorEvent, } /// Minimal usage summary for a turn. @@ -52,37 +52,37 @@ pub struct Usage { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] pub struct ItemStartedEvent { - pub item: ConversationItem, + pub item: ThreadItem, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] pub struct ItemCompletedEvent { - pub item: ConversationItem, + pub item: ThreadItem, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] pub struct ItemUpdatedEvent { - pub item: ConversationItem, + pub item: ThreadItem, } /// Fatal error emitted by the stream. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] -pub struct ConversationErrorEvent { +pub struct ThreadErrorEvent { pub message: String, } -/// Canonical representation of a conversation item and its domain-specific payload. +/// Canonical representation of a thread item and its domain-specific payload. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] -pub struct ConversationItem { +pub struct ThreadItem { pub id: String, #[serde(flatten)] - pub details: ConversationItemDetails, + pub details: ThreadItemDetails, } -/// Typed payloads for each supported conversation item type. +/// Typed payloads for each supported thread item type. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] #[serde(tag = "item_type", rename_all = "snake_case")] -pub enum ConversationItemDetails { +pub enum ThreadItemDetails { AssistantMessage(AssistantMessageItem), Reasoning(ReasoningItem), CommandExecution(CommandExecutionItem), @@ -93,7 +93,7 @@ pub enum ConversationItemDetails { Error(ErrorItem), } -/// Session conversation metadata. +/// Session metadata. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] pub struct SessionItem { pub session_id: String, diff --git a/codex-rs/exec/src/experimental_event_processor_with_json_output.rs b/codex-rs/exec/src/experimental_event_processor_with_json_output.rs index 89c10c3495..164ebecefa 100644 --- a/codex-rs/exec/src/experimental_event_processor_with_json_output.rs +++ b/codex-rs/exec/src/experimental_event_processor_with_json_output.rs @@ -8,10 +8,6 @@ use crate::event_processor::handle_last_message; use crate::exec_events::AssistantMessageItem; use crate::exec_events::CommandExecutionItem; use crate::exec_events::CommandExecutionStatus; -use crate::exec_events::ConversationErrorEvent; -use crate::exec_events::ConversationEvent; -use crate::exec_events::ConversationItem; -use crate::exec_events::ConversationItemDetails; use crate::exec_events::FileChangeItem; use crate::exec_events::FileUpdateChange; use crate::exec_events::ItemCompletedEvent; @@ -22,6 +18,10 @@ use crate::exec_events::McpToolCallStatus; use crate::exec_events::PatchApplyStatus; use crate::exec_events::PatchChangeKind; use crate::exec_events::ReasoningItem; +use crate::exec_events::ThreadErrorEvent; +use crate::exec_events::ThreadEvent; +use crate::exec_events::ThreadItem; +use crate::exec_events::ThreadItemDetails; use crate::exec_events::ThreadStartedEvent; use crate::exec_events::TodoItem; use crate::exec_events::TodoListItem; @@ -59,7 +59,7 @@ pub struct ExperimentalEventProcessorWithJsonOutput { running_todo_list: Option, last_total_token_usage: Option, running_mcp_tool_calls: HashMap, - last_critical_error: Option, + last_critical_error: Option, } #[derive(Debug, Clone)] @@ -95,7 +95,7 @@ impl ExperimentalEventProcessorWithJsonOutput { } } - pub fn collect_conversation_events(&mut self, event: &Event) -> Vec { + pub fn collect_thread_events(&mut self, event: &Event) -> Vec { match &event.msg { EventMsg::SessionConfigured(ev) => self.handle_session_configured(ev), EventMsg::AgentMessage(ev) => self.handle_agent_message(ev), @@ -115,13 +115,13 @@ impl ExperimentalEventProcessorWithJsonOutput { EventMsg::TaskStarted(ev) => self.handle_task_started(ev), EventMsg::TaskComplete(_) => self.handle_task_complete(), EventMsg::Error(ev) => { - let error = ConversationErrorEvent { + let error = ThreadErrorEvent { message: ev.message.clone(), }; self.last_critical_error = Some(error.clone()); - vec![ConversationEvent::Error(error)] + vec![ThreadEvent::Error(error)] } - EventMsg::StreamError(ev) => vec![ConversationEvent::Error(ConversationErrorEvent { + EventMsg::StreamError(ev) => vec![ThreadEvent::Error(ThreadErrorEvent { message: ev.message.clone(), })], EventMsg::PlanUpdate(ev) => self.handle_plan_update(ev), @@ -137,43 +137,36 @@ impl ExperimentalEventProcessorWithJsonOutput { ) } - fn handle_session_configured( - &self, - payload: &SessionConfiguredEvent, - ) -> Vec { - vec![ConversationEvent::ThreadStarted(ThreadStartedEvent { + fn handle_session_configured(&self, payload: &SessionConfiguredEvent) -> Vec { + vec![ThreadEvent::ThreadStarted(ThreadStartedEvent { thread_id: payload.session_id.to_string(), })] } - fn handle_agent_message(&self, payload: &AgentMessageEvent) -> Vec { - let item = ConversationItem { + fn handle_agent_message(&self, payload: &AgentMessageEvent) -> Vec { + let item = ThreadItem { id: self.get_next_item_id(), - details: ConversationItemDetails::AssistantMessage(AssistantMessageItem { + details: ThreadItemDetails::AssistantMessage(AssistantMessageItem { text: payload.message.clone(), }), }; - vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { - item, - })] + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { item })] } - fn handle_reasoning_event(&self, ev: &AgentReasoningEvent) -> Vec { - let item = ConversationItem { + fn handle_reasoning_event(&self, ev: &AgentReasoningEvent) -> Vec { + let item = ThreadItem { id: self.get_next_item_id(), - details: ConversationItemDetails::Reasoning(ReasoningItem { + details: ThreadItemDetails::Reasoning(ReasoningItem { text: ev.text.clone(), }), }; - vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { - item, - })] + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { item })] } - fn handle_exec_command_begin(&mut self, ev: &ExecCommandBeginEvent) -> Vec { + fn handle_exec_command_begin(&mut self, ev: &ExecCommandBeginEvent) -> Vec { let item_id = self.get_next_item_id(); let command_string = match shlex::try_join(ev.command.iter().map(String::as_str)) { @@ -195,9 +188,9 @@ impl ExperimentalEventProcessorWithJsonOutput { }, ); - let item = ConversationItem { + let item = ThreadItem { id: item_id, - details: ConversationItemDetails::CommandExecution(CommandExecutionItem { + details: ThreadItemDetails::CommandExecution(CommandExecutionItem { command: command_string, aggregated_output: String::new(), exit_code: None, @@ -205,10 +198,10 @@ impl ExperimentalEventProcessorWithJsonOutput { }), }; - vec![ConversationEvent::ItemStarted(ItemStartedEvent { item })] + vec![ThreadEvent::ItemStarted(ItemStartedEvent { item })] } - fn handle_mcp_tool_call_begin(&mut self, ev: &McpToolCallBeginEvent) -> Vec { + fn handle_mcp_tool_call_begin(&mut self, ev: &McpToolCallBeginEvent) -> Vec { let item_id = self.get_next_item_id(); let server = ev.invocation.server.clone(); let tool = ev.invocation.tool.clone(); @@ -222,19 +215,19 @@ impl ExperimentalEventProcessorWithJsonOutput { }, ); - let item = ConversationItem { + let item = ThreadItem { id: item_id, - details: ConversationItemDetails::McpToolCall(McpToolCallItem { + details: ThreadItemDetails::McpToolCall(McpToolCallItem { server, tool, status: McpToolCallStatus::InProgress, }), }; - vec![ConversationEvent::ItemStarted(ItemStartedEvent { item })] + vec![ThreadEvent::ItemStarted(ItemStartedEvent { item })] } - fn handle_mcp_tool_call_end(&mut self, ev: &McpToolCallEndEvent) -> Vec { + fn handle_mcp_tool_call_end(&mut self, ev: &McpToolCallEndEvent) -> Vec { let status = if ev.is_success() { McpToolCallStatus::Completed } else { @@ -256,21 +249,19 @@ impl ExperimentalEventProcessorWithJsonOutput { } }; - let item = ConversationItem { + let item = ThreadItem { id: item_id, - details: ConversationItemDetails::McpToolCall(McpToolCallItem { + details: ThreadItemDetails::McpToolCall(McpToolCallItem { server, tool, status, }), }; - vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { - item, - })] + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { item })] } - fn handle_patch_apply_begin(&mut self, ev: &PatchApplyBeginEvent) -> Vec { + fn handle_patch_apply_begin(&mut self, ev: &PatchApplyBeginEvent) -> Vec { self.running_patch_applies .insert(ev.call_id.clone(), ev.clone()); @@ -285,17 +276,17 @@ impl ExperimentalEventProcessorWithJsonOutput { } } - fn handle_patch_apply_end(&mut self, ev: &PatchApplyEndEvent) -> Vec { + fn handle_patch_apply_end(&mut self, ev: &PatchApplyEndEvent) -> Vec { if let Some(running_patch_apply) = self.running_patch_applies.remove(&ev.call_id) { let status = if ev.success { PatchApplyStatus::Completed } else { PatchApplyStatus::Failed }; - let item = ConversationItem { + let item = ThreadItem { id: self.get_next_item_id(), - details: ConversationItemDetails::FileChange(FileChangeItem { + details: ThreadItemDetails::FileChange(FileChangeItem { changes: running_patch_apply .changes .iter() @@ -308,15 +299,13 @@ impl ExperimentalEventProcessorWithJsonOutput { }), }; - return vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { - item, - })]; + return vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { item })]; } Vec::new() } - fn handle_exec_command_end(&mut self, ev: &ExecCommandEndEvent) -> Vec { + fn handle_exec_command_end(&mut self, ev: &ExecCommandEndEvent) -> Vec { let Some(RunningCommand { command, item_id }) = self.running_commands.remove(&ev.call_id) else { warn!( @@ -330,10 +319,10 @@ impl ExperimentalEventProcessorWithJsonOutput { } else { CommandExecutionStatus::Failed }; - let item = ConversationItem { + let item = ThreadItem { id: item_id, - details: ConversationItemDetails::CommandExecution(CommandExecutionItem { + details: ThreadItemDetails::CommandExecution(CommandExecutionItem { command, aggregated_output: ev.aggregated_output.clone(), exit_code: Some(ev.exit_code), @@ -341,9 +330,7 @@ impl ExperimentalEventProcessorWithJsonOutput { }), }; - vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { - item, - })] + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { item })] } fn todo_items_from_plan(&self, args: &UpdatePlanArgs) -> Vec { @@ -356,16 +343,16 @@ impl ExperimentalEventProcessorWithJsonOutput { .collect() } - fn handle_plan_update(&mut self, args: &UpdatePlanArgs) -> Vec { + fn handle_plan_update(&mut self, args: &UpdatePlanArgs) -> Vec { let items = self.todo_items_from_plan(args); if let Some(running) = &mut self.running_todo_list { running.items = items.clone(); - let item = ConversationItem { + let item = ThreadItem { id: running.item_id.clone(), - details: ConversationItemDetails::TodoList(TodoListItem { items }), + details: ThreadItemDetails::TodoList(TodoListItem { items }), }; - return vec![ConversationEvent::ItemUpdated(ItemUpdatedEvent { item })]; + return vec![ThreadEvent::ItemUpdated(ItemUpdatedEvent { item })]; } let item_id = self.get_next_item_id(); @@ -373,19 +360,19 @@ impl ExperimentalEventProcessorWithJsonOutput { item_id: item_id.clone(), items: items.clone(), }); - let item = ConversationItem { + let item = ThreadItem { id: item_id, - details: ConversationItemDetails::TodoList(TodoListItem { items }), + details: ThreadItemDetails::TodoList(TodoListItem { items }), }; - vec![ConversationEvent::ItemStarted(ItemStartedEvent { item })] + vec![ThreadEvent::ItemStarted(ItemStartedEvent { item })] } - fn handle_task_started(&mut self, _: &TaskStartedEvent) -> Vec { + fn handle_task_started(&mut self, _: &TaskStartedEvent) -> Vec { self.last_critical_error = None; - vec![ConversationEvent::TurnStarted(TurnStartedEvent {})] + vec![ThreadEvent::TurnStarted(TurnStartedEvent {})] } - fn handle_task_complete(&mut self) -> Vec { + fn handle_task_complete(&mut self) -> Vec { let usage = if let Some(u) = &self.last_total_token_usage { Usage { input_tokens: u.input_tokens, @@ -399,23 +386,19 @@ impl ExperimentalEventProcessorWithJsonOutput { let mut items = Vec::new(); if let Some(running) = self.running_todo_list.take() { - let item = ConversationItem { + let item = ThreadItem { id: running.item_id, - details: ConversationItemDetails::TodoList(TodoListItem { + details: ThreadItemDetails::TodoList(TodoListItem { items: running.items, }), }; - items.push(ConversationEvent::ItemCompleted(ItemCompletedEvent { - item, - })); + items.push(ThreadEvent::ItemCompleted(ItemCompletedEvent { item })); } if let Some(error) = self.last_critical_error.take() { - items.push(ConversationEvent::TurnFailed(TurnFailedEvent { error })); + items.push(ThreadEvent::TurnFailed(TurnFailedEvent { error })); } else { - items.push(ConversationEvent::TurnCompleted(TurnCompletedEvent { - usage, - })); + items.push(ThreadEvent::TurnCompleted(TurnCompletedEvent { usage })); } items @@ -431,7 +414,7 @@ impl EventProcessor for ExperimentalEventProcessorWithJsonOutput { } fn process_event(&mut self, event: Event) -> CodexStatus { - let aggregated = self.collect_conversation_events(&event); + let aggregated = self.collect_thread_events(&event); for conv_event in aggregated { match serde_json::to_string(&conv_event) { Ok(line) => { diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index 198be44ad7..c462879767 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -15,10 +15,6 @@ use codex_core::protocol::SessionConfiguredEvent; use codex_exec::exec_events::AssistantMessageItem; use codex_exec::exec_events::CommandExecutionItem; use codex_exec::exec_events::CommandExecutionStatus; -use codex_exec::exec_events::ConversationErrorEvent; -use codex_exec::exec_events::ConversationEvent; -use codex_exec::exec_events::ConversationItem; -use codex_exec::exec_events::ConversationItemDetails; use codex_exec::exec_events::ItemCompletedEvent; use codex_exec::exec_events::ItemStartedEvent; use codex_exec::exec_events::ItemUpdatedEvent; @@ -27,6 +23,10 @@ use codex_exec::exec_events::McpToolCallStatus; use codex_exec::exec_events::PatchApplyStatus; use codex_exec::exec_events::PatchChangeKind; use codex_exec::exec_events::ReasoningItem; +use codex_exec::exec_events::ThreadErrorEvent; +use codex_exec::exec_events::ThreadEvent; +use codex_exec::exec_events::ThreadItem; +use codex_exec::exec_events::ThreadItemDetails; use codex_exec::exec_events::ThreadStartedEvent; use codex_exec::exec_events::TodoItem as ExecTodoItem; use codex_exec::exec_events::TodoListItem as ExecTodoListItem; @@ -67,10 +67,10 @@ fn session_configured_produces_thread_started_event() { rollout_path, }), ); - let out = ep.collect_conversation_events(&ev); + let out = ep.collect_thread_events(&ev); assert_eq!( out, - vec![ConversationEvent::ThreadStarted(ThreadStartedEvent { + vec![ThreadEvent::ThreadStarted(ThreadStartedEvent { thread_id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(), })] ); @@ -79,17 +79,14 @@ fn session_configured_produces_thread_started_event() { #[test] fn task_started_produces_turn_started_event() { let mut ep = ExperimentalEventProcessorWithJsonOutput::new(None); - let out = ep.collect_conversation_events(&event( + let out = ep.collect_thread_events(&event( "t1", EventMsg::TaskStarted(codex_core::protocol::TaskStartedEvent { model_context_window: Some(32_000), }), )); - assert_eq!( - out, - vec![ConversationEvent::TurnStarted(TurnStartedEvent {})] - ); + assert_eq!(out, vec![ThreadEvent::TurnStarted(TurnStartedEvent {})]); } #[test] @@ -117,13 +114,13 @@ fn plan_update_emits_todo_list_started_updated_and_completed() { ], }), ); - let out_first = ep.collect_conversation_events(&first); + let out_first = ep.collect_thread_events(&first); assert_eq!( out_first, - vec![ConversationEvent::ItemStarted(ItemStartedEvent { - item: ConversationItem { + vec![ThreadEvent::ItemStarted(ItemStartedEvent { + item: ThreadItem { id: "item_0".to_string(), - details: ConversationItemDetails::TodoList(ExecTodoListItem { + details: ThreadItemDetails::TodoList(ExecTodoListItem { items: vec![ ExecTodoItem { text: "step one".to_string(), @@ -156,13 +153,13 @@ fn plan_update_emits_todo_list_started_updated_and_completed() { ], }), ); - let out_second = ep.collect_conversation_events(&second); + let out_second = ep.collect_thread_events(&second); assert_eq!( out_second, - vec![ConversationEvent::ItemUpdated(ItemUpdatedEvent { - item: ConversationItem { + vec![ThreadEvent::ItemUpdated(ItemUpdatedEvent { + item: ThreadItem { id: "item_0".to_string(), - details: ConversationItemDetails::TodoList(ExecTodoListItem { + details: ThreadItemDetails::TodoList(ExecTodoListItem { items: vec![ ExecTodoItem { text: "step one".to_string(), @@ -185,14 +182,14 @@ fn plan_update_emits_todo_list_started_updated_and_completed() { last_agent_message: None, }), ); - let out_complete = ep.collect_conversation_events(&complete); + let out_complete = ep.collect_thread_events(&complete); assert_eq!( out_complete, vec![ - ConversationEvent::ItemCompleted(ItemCompletedEvent { - item: ConversationItem { + ThreadEvent::ItemCompleted(ItemCompletedEvent { + item: ThreadItem { id: "item_0".to_string(), - details: ConversationItemDetails::TodoList(ExecTodoListItem { + details: ThreadItemDetails::TodoList(ExecTodoListItem { items: vec![ ExecTodoItem { text: "step one".to_string(), @@ -206,7 +203,7 @@ fn plan_update_emits_todo_list_started_updated_and_completed() { }), }, }), - ConversationEvent::TurnCompleted(TurnCompletedEvent { + ThreadEvent::TurnCompleted(TurnCompletedEvent { usage: Usage::default(), }), ] @@ -229,13 +226,13 @@ fn mcp_tool_call_begin_and_end_emit_item_events() { invocation: invocation.clone(), }), ); - let begin_events = ep.collect_conversation_events(&begin); + let begin_events = ep.collect_thread_events(&begin); assert_eq!( begin_events, - vec![ConversationEvent::ItemStarted(ItemStartedEvent { - item: ConversationItem { + vec![ThreadEvent::ItemStarted(ItemStartedEvent { + item: ThreadItem { id: "item_0".to_string(), - details: ConversationItemDetails::McpToolCall(McpToolCallItem { + details: ThreadItemDetails::McpToolCall(McpToolCallItem { server: "server_a".to_string(), tool: "tool_x".to_string(), status: McpToolCallStatus::InProgress, @@ -257,13 +254,13 @@ fn mcp_tool_call_begin_and_end_emit_item_events() { }), }), ); - let end_events = ep.collect_conversation_events(&end); + let end_events = ep.collect_thread_events(&end); assert_eq!( end_events, - vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { - item: ConversationItem { + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { + item: ThreadItem { id: "item_0".to_string(), - details: ConversationItemDetails::McpToolCall(McpToolCallItem { + details: ThreadItemDetails::McpToolCall(McpToolCallItem { server: "server_a".to_string(), tool: "tool_x".to_string(), status: McpToolCallStatus::Completed, @@ -289,7 +286,7 @@ fn mcp_tool_call_failure_sets_failed_status() { invocation: invocation.clone(), }), ); - ep.collect_conversation_events(&begin); + ep.collect_thread_events(&begin); let end = event( "m4", @@ -300,13 +297,13 @@ fn mcp_tool_call_failure_sets_failed_status() { result: Err("tool exploded".to_string()), }), ); - let events = ep.collect_conversation_events(&end); + let events = ep.collect_thread_events(&end); assert_eq!( events, - vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { - item: ConversationItem { + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { + item: ThreadItem { id: "item_0".to_string(), - details: ConversationItemDetails::McpToolCall(McpToolCallItem { + details: ThreadItemDetails::McpToolCall(McpToolCallItem { server: "server_b".to_string(), tool: "tool_y".to_string(), status: McpToolCallStatus::Failed, @@ -335,14 +332,14 @@ fn plan_update_after_complete_starts_new_todo_list_with_new_id() { }], }), ); - let _ = ep.collect_conversation_events(&start); + let _ = ep.collect_thread_events(&start); let complete = event( "t2", EventMsg::TaskComplete(codex_core::protocol::TaskCompleteEvent { last_agent_message: None, }), ); - let _ = ep.collect_conversation_events(&complete); + let _ = ep.collect_thread_events(&complete); // Second turn: a new todo list should have a new id let start_again = event( @@ -355,10 +352,10 @@ fn plan_update_after_complete_starts_new_todo_list_with_new_id() { }], }), ); - let out = ep.collect_conversation_events(&start_again); + let out = ep.collect_thread_events(&start_again); match &out[0] { - ConversationEvent::ItemStarted(ItemStartedEvent { item }) => { + ThreadEvent::ItemStarted(ItemStartedEvent { item }) => { assert_eq!(&item.id, "item_1"); } other => panic!("unexpected event: {other:?}"), @@ -374,13 +371,13 @@ fn agent_reasoning_produces_item_completed_reasoning() { text: "thinking...".to_string(), }), ); - let out = ep.collect_conversation_events(&ev); + let out = ep.collect_thread_events(&ev); assert_eq!( out, - vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { - item: ConversationItem { + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { + item: ThreadItem { id: "item_0".to_string(), - details: ConversationItemDetails::Reasoning(ReasoningItem { + details: ThreadItemDetails::Reasoning(ReasoningItem { text: "thinking...".to_string(), }), }, @@ -397,13 +394,13 @@ fn agent_message_produces_item_completed_assistant_message() { message: "hello".to_string(), }), ); - let out = ep.collect_conversation_events(&ev); + let out = ep.collect_thread_events(&ev); assert_eq!( out, - vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { - item: ConversationItem { + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { + item: ThreadItem { id: "item_0".to_string(), - details: ConversationItemDetails::AssistantMessage(AssistantMessageItem { + details: ThreadItemDetails::AssistantMessage(AssistantMessageItem { text: "hello".to_string(), }), }, @@ -414,7 +411,7 @@ fn agent_message_produces_item_completed_assistant_message() { #[test] fn error_event_produces_error() { let mut ep = ExperimentalEventProcessorWithJsonOutput::new(None); - let out = ep.collect_conversation_events(&event( + let out = ep.collect_thread_events(&event( "e1", EventMsg::Error(codex_core::protocol::ErrorEvent { message: "boom".to_string(), @@ -422,7 +419,7 @@ fn error_event_produces_error() { )); assert_eq!( out, - vec![ConversationEvent::Error(ConversationErrorEvent { + vec![ThreadEvent::Error(ThreadErrorEvent { message: "boom".to_string(), })] ); @@ -431,7 +428,7 @@ fn error_event_produces_error() { #[test] fn stream_error_event_produces_error() { let mut ep = ExperimentalEventProcessorWithJsonOutput::new(None); - let out = ep.collect_conversation_events(&event( + let out = ep.collect_thread_events(&event( "e1", EventMsg::StreamError(codex_core::protocol::StreamErrorEvent { message: "retrying".to_string(), @@ -439,7 +436,7 @@ fn stream_error_event_produces_error() { )); assert_eq!( out, - vec![ConversationEvent::Error(ConversationErrorEvent { + vec![ThreadEvent::Error(ThreadErrorEvent { message: "retrying".to_string(), })] ); @@ -456,8 +453,8 @@ fn error_followed_by_task_complete_produces_turn_failed() { }), ); assert_eq!( - ep.collect_conversation_events(&error_event), - vec![ConversationEvent::Error(ConversationErrorEvent { + ep.collect_thread_events(&error_event), + vec![ThreadEvent::Error(ThreadErrorEvent { message: "boom".to_string(), })] ); @@ -469,9 +466,9 @@ fn error_followed_by_task_complete_produces_turn_failed() { }), ); assert_eq!( - ep.collect_conversation_events(&complete_event), - vec![ConversationEvent::TurnFailed(TurnFailedEvent { - error: ConversationErrorEvent { + ep.collect_thread_events(&complete_event), + vec![ThreadEvent::TurnFailed(TurnFailedEvent { + error: ThreadErrorEvent { message: "boom".to_string(), }, })] @@ -492,13 +489,13 @@ fn exec_command_end_success_produces_completed_command_item() { parsed_cmd: Vec::new(), }), ); - let out_begin = ep.collect_conversation_events(&begin); + let out_begin = ep.collect_thread_events(&begin); assert_eq!( out_begin, - vec![ConversationEvent::ItemStarted(ItemStartedEvent { - item: ConversationItem { + vec![ThreadEvent::ItemStarted(ItemStartedEvent { + item: ThreadItem { id: "item_0".to_string(), - details: ConversationItemDetails::CommandExecution(CommandExecutionItem { + details: ThreadItemDetails::CommandExecution(CommandExecutionItem { command: "bash -lc 'echo hi'".to_string(), aggregated_output: String::new(), exit_code: None, @@ -521,13 +518,13 @@ fn exec_command_end_success_produces_completed_command_item() { formatted_output: String::new(), }), ); - let out_ok = ep.collect_conversation_events(&end_ok); + let out_ok = ep.collect_thread_events(&end_ok); assert_eq!( out_ok, - vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { - item: ConversationItem { + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { + item: ThreadItem { id: "item_0".to_string(), - details: ConversationItemDetails::CommandExecution(CommandExecutionItem { + details: ThreadItemDetails::CommandExecution(CommandExecutionItem { command: "bash -lc 'echo hi'".to_string(), aggregated_output: "hi\n".to_string(), exit_code: Some(0), @@ -553,11 +550,11 @@ fn exec_command_end_failure_produces_failed_command_item() { }), ); assert_eq!( - ep.collect_conversation_events(&begin), - vec![ConversationEvent::ItemStarted(ItemStartedEvent { - item: ConversationItem { + ep.collect_thread_events(&begin), + vec![ThreadEvent::ItemStarted(ItemStartedEvent { + item: ThreadItem { id: "item_0".to_string(), - details: ConversationItemDetails::CommandExecution(CommandExecutionItem { + details: ThreadItemDetails::CommandExecution(CommandExecutionItem { command: "sh -c 'exit 1'".to_string(), aggregated_output: String::new(), exit_code: None, @@ -580,13 +577,13 @@ fn exec_command_end_failure_produces_failed_command_item() { formatted_output: String::new(), }), ); - let out_fail = ep.collect_conversation_events(&end_fail); + let out_fail = ep.collect_thread_events(&end_fail); assert_eq!( out_fail, - vec![ConversationEvent::ItemCompleted(ItemCompletedEvent { - item: ConversationItem { + vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { + item: ThreadItem { id: "item_0".to_string(), - details: ConversationItemDetails::CommandExecution(CommandExecutionItem { + details: ThreadItemDetails::CommandExecution(CommandExecutionItem { command: "sh -c 'exit 1'".to_string(), aggregated_output: String::new(), exit_code: Some(1), @@ -601,7 +598,7 @@ fn exec_command_end_failure_produces_failed_command_item() { fn exec_command_end_without_begin_is_ignored() { let mut ep = ExperimentalEventProcessorWithJsonOutput::new(None); - // End event arrives without a prior Begin; should produce no conversation events. + // End event arrives without a prior Begin; should produce no thread events. let end_only = event( "c1", EventMsg::ExecCommandEnd(ExecCommandEndEvent { @@ -614,7 +611,7 @@ fn exec_command_end_without_begin_is_ignored() { formatted_output: String::new(), }), ); - let out = ep.collect_conversation_events(&end_only); + let out = ep.collect_thread_events(&end_only); assert!(out.is_empty()); } @@ -653,7 +650,7 @@ fn patch_apply_success_produces_item_completed_patchapply() { changes: changes.clone(), }), ); - let out_begin = ep.collect_conversation_events(&begin); + let out_begin = ep.collect_thread_events(&begin); assert!(out_begin.is_empty()); // End (success) -> item.completed (item_0) @@ -666,15 +663,15 @@ fn patch_apply_success_produces_item_completed_patchapply() { success: true, }), ); - let out_end = ep.collect_conversation_events(&end); + let out_end = ep.collect_thread_events(&end); assert_eq!(out_end.len(), 1); // Validate structure without relying on HashMap iteration order match &out_end[0] { - ConversationEvent::ItemCompleted(ItemCompletedEvent { item }) => { + ThreadEvent::ItemCompleted(ItemCompletedEvent { item }) => { assert_eq!(&item.id, "item_0"); match &item.details { - ConversationItemDetails::FileChange(file_update) => { + ThreadItemDetails::FileChange(file_update) => { assert_eq!(file_update.status, PatchApplyStatus::Completed); let mut actual: Vec<(String, PatchChangeKind)> = file_update @@ -722,7 +719,7 @@ fn patch_apply_failure_produces_item_completed_patchapply_failed() { changes: changes.clone(), }), ); - assert!(ep.collect_conversation_events(&begin).is_empty()); + assert!(ep.collect_thread_events(&begin).is_empty()); // End (failure) -> item.completed (item_0) with Failed status let end = event( @@ -734,14 +731,14 @@ fn patch_apply_failure_produces_item_completed_patchapply_failed() { success: false, }), ); - let out_end = ep.collect_conversation_events(&end); + let out_end = ep.collect_thread_events(&end); assert_eq!(out_end.len(), 1); match &out_end[0] { - ConversationEvent::ItemCompleted(ItemCompletedEvent { item }) => { + ThreadEvent::ItemCompleted(ItemCompletedEvent { item }) => { assert_eq!(&item.id, "item_0"); match &item.details { - ConversationItemDetails::FileChange(file_update) => { + ThreadItemDetails::FileChange(file_update) => { assert_eq!(file_update.status, PatchApplyStatus::Failed); assert_eq!(file_update.changes.len(), 1); assert_eq!(file_update.changes[0].path, "file.txt".to_string()); @@ -778,10 +775,7 @@ fn task_complete_produces_turn_completed_with_usage() { rate_limits: None, }), ); - assert!( - ep.collect_conversation_events(&token_count_event) - .is_empty() - ); + assert!(ep.collect_thread_events(&token_count_event).is_empty()); // Then TaskComplete should produce turn.completed with the captured usage. let complete_event = event( @@ -790,10 +784,10 @@ fn task_complete_produces_turn_completed_with_usage() { last_agent_message: Some("done".to_string()), }), ); - let out = ep.collect_conversation_events(&complete_event); + let out = ep.collect_thread_events(&complete_event); assert_eq!( out, - vec![ConversationEvent::TurnCompleted(TurnCompletedEvent { + vec![ThreadEvent::TurnCompleted(TurnCompletedEvent { usage: Usage { input_tokens: 1200, cached_input_tokens: 200, diff --git a/sdk/typescript/samples/basic_streaming.ts b/sdk/typescript/samples/basic_streaming.ts index 41499b3e84..801b52e76f 100755 --- a/sdk/typescript/samples/basic_streaming.ts +++ b/sdk/typescript/samples/basic_streaming.ts @@ -4,7 +4,7 @@ import { createInterface } from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; import { Codex } from "@openai/codex-sdk"; -import type { ConversationEvent, ConversationItem } from "@openai/codex-sdk"; +import type { ThreadEvent, ThreadItem } from "@openai/codex-sdk"; import path from "node:path"; const executablePath = @@ -15,7 +15,7 @@ const codex = new Codex({ executablePath }); const thread = codex.startThread(); const rl = createInterface({ input, output }); -const handleItemCompleted = (item: ConversationItem): void => { +const handleItemCompleted = (item: ThreadItem): void => { switch (item.item_type) { case "assistant_message": console.log(`Assistant: ${item.text}`); @@ -37,7 +37,7 @@ const handleItemCompleted = (item: ConversationItem): void => { } }; -const handleItemUpdated = (item: ConversationItem): void => { +const handleItemUpdated = (item: ThreadItem): void => { switch (item.item_type) { case "todo_list": { console.log(`Todo:`); @@ -49,7 +49,7 @@ const handleItemUpdated = (item: ConversationItem): void => { } }; -const handleEvent = (event: ConversationEvent): void => { +const handleEvent = (event: ThreadEvent): void => { switch (event.type) { case "item.completed": handleItemCompleted(event.item); @@ -63,6 +63,9 @@ const handleEvent = (event: ConversationEvent): void => { `Used ${event.usage.input_tokens} input tokens, ${event.usage.cached_input_tokens} cached input tokens, ${event.usage.output_tokens} output tokens.`, ); break; + case "turn.failed": + console.error(`Turn failed: ${event.error.message}`); + break; } }; diff --git a/sdk/typescript/src/events.ts b/sdk/typescript/src/events.ts index 89402bc75e..344e348a0f 100644 --- a/sdk/typescript/src/events.ts +++ b/sdk/typescript/src/events.ts @@ -1,10 +1,10 @@ // based on event types from codex-rs/exec/src/exec_events.rs -import type { ConversationItem } from "./items"; +import type { ThreadItem } from "./items"; -export type SessionCreatedEvent = { - type: "session.created"; - session_id: string; +export type ThreadStartedEvent = { + type: "thread.started"; + thread_id: string; }; export type TurnStartedEvent = { @@ -22,31 +22,41 @@ export type TurnCompletedEvent = { usage: Usage; }; +export type TurnFailedEvent = { + type: "turn.failed"; + error: ThreadError; +}; + export type ItemStartedEvent = { type: "item.started"; - item: ConversationItem; + item: ThreadItem; }; export type ItemUpdatedEvent = { type: "item.updated"; - item: ConversationItem; + item: ThreadItem; }; export type ItemCompletedEvent = { type: "item.completed"; - item: ConversationItem; + item: ThreadItem; }; -export type ConversationErrorEvent = { +export type ThreadError = { + message: string; +}; + +export type ThreadErrorEvent = { type: "error"; message: string; }; -export type ConversationEvent = - | SessionCreatedEvent +export type ThreadEvent = + | ThreadStartedEvent | TurnStartedEvent | TurnCompletedEvent + | TurnFailedEvent | ItemStartedEvent | ItemUpdatedEvent | ItemCompletedEvent - | ConversationErrorEvent; + | ThreadErrorEvent; diff --git a/sdk/typescript/src/exec.ts b/sdk/typescript/src/exec.ts index 9a078aa9c6..0369f8670f 100644 --- a/sdk/typescript/src/exec.ts +++ b/sdk/typescript/src/exec.ts @@ -6,7 +6,7 @@ export type CodexExecArgs = { baseUrl?: string; apiKey?: string; - sessionId?: string | null; + threadId?: string | null; }; export class CodexExec { @@ -17,8 +17,8 @@ export class CodexExec { async *run(args: CodexExecArgs): AsyncGenerator { const commandArgs: string[] = ["exec", "--experimental-json"]; - if (args.sessionId) { - commandArgs.push("resume", args.sessionId, args.input); + if (args.threadId) { + commandArgs.push("resume", args.threadId, args.input); } else { commandArgs.push(args.input); } diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index f46471fd04..02bbfa1b33 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -1,15 +1,17 @@ export type { - ConversationEvent, - SessionCreatedEvent, + ThreadEvent, + ThreadStartedEvent, TurnStartedEvent, TurnCompletedEvent, + TurnFailedEvent, ItemStartedEvent, ItemUpdatedEvent, ItemCompletedEvent, - ConversationErrorEvent, + ThreadError, + ThreadErrorEvent, } from "./events"; export type { - ConversationItem, + ThreadItem, AssistantMessageItem, ReasoningItem, CommandExecutionItem, diff --git a/sdk/typescript/src/items.ts b/sdk/typescript/src/items.ts index 82fb0bb93c..546dd66ffd 100644 --- a/sdk/typescript/src/items.ts +++ b/sdk/typescript/src/items.ts @@ -78,17 +78,7 @@ export type SessionItem = { session_id: string; }; -export type ConversationItem = - | AssistantMessageItem - | ReasoningItem - | CommandExecutionItem - | FileChangeItem - | McpToolCallItem - | WebSearchItem - | TodoListItem - | ErrorItem; - -export type ConversationItemDetails = +export type ThreadItem = | AssistantMessageItem | ReasoningItem | CommandExecutionItem diff --git a/sdk/typescript/src/thread.ts b/sdk/typescript/src/thread.ts index 2091f6774f..cd19a94288 100644 --- a/sdk/typescript/src/thread.ts +++ b/sdk/typescript/src/thread.ts @@ -1,15 +1,15 @@ import { CodexOptions } from "./codexOptions"; -import { ConversationEvent } from "./events"; +import { ThreadEvent } from "./events"; import { CodexExec } from "./exec"; -import { ConversationItem } from "./items"; +import { ThreadItem } from "./items"; export type RunResult = { - items: ConversationItem[]; + items: ThreadItem[]; finalResponse: string; }; export type RunStreamedResult = { - events: AsyncGenerator; + events: AsyncGenerator; }; export type Input = string; @@ -29,17 +29,17 @@ export class Thread { return { events: this.runStreamedInternal(input) }; } - private async *runStreamedInternal(input: string): AsyncGenerator { + private async *runStreamedInternal(input: string): AsyncGenerator { const generator = this.exec.run({ input, baseUrl: this.options.baseUrl, apiKey: this.options.apiKey, - sessionId: this.id, + threadId: this.id, }); for await (const item of generator) { - const parsed = JSON.parse(item) as ConversationEvent; - if (parsed.type === "session.created") { - this.id = parsed.session_id; + const parsed = JSON.parse(item) as ThreadEvent; + if (parsed.type === "thread.started") { + this.id = parsed.thread_id; } yield parsed; } @@ -47,7 +47,7 @@ export class Thread { async run(input: string): Promise { const generator = this.runStreamedInternal(input); - const items: ConversationItem[] = []; + const items: ThreadItem[] = []; let finalResponse: string = ""; for await (const event of generator) { if (event.type === "item.completed") { diff --git a/sdk/typescript/tests/run.test.ts b/sdk/typescript/tests/run.test.ts index 9f7fb792b9..7f6c57c2d3 100644 --- a/sdk/typescript/tests/run.test.ts +++ b/sdk/typescript/tests/run.test.ts @@ -15,7 +15,7 @@ import { const codexExecPath = path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex"); describe("Codex", () => { - it("returns session events", async () => { + it("returns thread events", async () => { const { url, close } = await startResponsesTestProxy({ statusCode: 200, responseBodies: [sse(responseStarted(), assistantMessage("Hi!"), responseCompleted())], @@ -65,7 +65,7 @@ describe("Codex", () => { await thread.run("first input"); await thread.run("second input"); - // Check second request continues conversation + // Check second request continues the same thread expect(requests.length).toBeGreaterThanOrEqual(2); const secondRequest = requests[1]; expect(secondRequest).toBeDefined(); diff --git a/sdk/typescript/tests/runStreamed.test.ts b/sdk/typescript/tests/runStreamed.test.ts index cb10435a86..85b5cf38df 100644 --- a/sdk/typescript/tests/runStreamed.test.ts +++ b/sdk/typescript/tests/runStreamed.test.ts @@ -3,7 +3,7 @@ import path from "path"; import { describe, expect, it } from "@jest/globals"; import { Codex } from "../src/codex"; -import { ConversationEvent } from "../src/index"; +import { ThreadEvent } from "../src/index"; import { assistantMessage, @@ -16,7 +16,7 @@ import { const codexExecPath = path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex"); describe("Codex", () => { - it("returns session events", async () => { + it("returns thread events", async () => { const { url, close } = await startResponsesTestProxy({ statusCode: 200, responseBodies: [sse(responseStarted(), assistantMessage("Hi!"), responseCompleted())], @@ -28,15 +28,15 @@ describe("Codex", () => { const thread = client.startThread(); const result = await thread.runStreamed("Hello, world!"); - const events: ConversationEvent[] = []; + const events: ThreadEvent[] = []; for await (const event of result.events) { events.push(event); } expect(events).toEqual([ { - type: "session.created", - session_id: expect.any(String), + type: "thread.started", + thread_id: expect.any(String), }, { type: "turn.started", @@ -91,7 +91,7 @@ describe("Codex", () => { const second = await thread.runStreamed("second input"); await drainEvents(second.events); - // Check second request continues conversation + // Check second request continues the same thread expect(requests.length).toBeGreaterThanOrEqual(2); const secondRequest = requests[1]; expect(secondRequest).toBeDefined(); @@ -159,7 +159,7 @@ describe("Codex", () => { }); }); -async function drainEvents(events: AsyncGenerator): Promise { +async function drainEvents(events: AsyncGenerator): Promise { let done = false; do { done = (await events.next()).done ?? false; From c09e131653652bc0357556284c67a644edcf7a79 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 29 Sep 2025 20:59:19 -0700 Subject: [PATCH 18/23] Set originator for codex exec (#4485) Distinct from the main CLI. --- codex-rs/core/src/default_client.rs | 38 +++++++++++++++++++++++---- codex-rs/core/src/otel_init.rs | 4 +-- codex-rs/core/src/rollout/recorder.rs | 4 +-- codex-rs/exec/src/lib.rs | 5 ++++ codex-rs/login/src/server.rs | 4 +-- 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/codex-rs/core/src/default_client.rs b/codex-rs/core/src/default_client.rs index 5ce08d9d5d..5a6ea8d826 100644 --- a/codex-rs/core/src/default_client.rs +++ b/codex-rs/core/src/default_client.rs @@ -2,6 +2,7 @@ use crate::spawn::CODEX_SANDBOX_ENV_VAR; use reqwest::header::HeaderValue; use std::sync::LazyLock; use std::sync::Mutex; +use std::sync::OnceLock; /// Set this to add a suffix to the User-Agent string. /// @@ -26,8 +27,15 @@ pub struct Originator { pub value: String, pub header_value: HeaderValue, } +static ORIGINATOR: OnceLock = OnceLock::new(); -pub static ORIGINATOR: LazyLock = LazyLock::new(|| { +#[derive(Debug)] +pub enum SetOriginatorError { + InvalidHeaderValue, + AlreadyInitialized, +} + +fn init_originator_from_env() -> Originator { let default = "codex_cli_rs"; let value = std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR) .unwrap_or_else(|_| default.to_string()); @@ -45,14 +53,34 @@ pub static ORIGINATOR: LazyLock = LazyLock::new(|| { } } } -}); +} + +fn build_originator(value: String) -> Result { + let header_value = + HeaderValue::from_str(&value).map_err(|_| SetOriginatorError::InvalidHeaderValue)?; + Ok(Originator { + value, + header_value, + }) +} + +pub fn set_default_originator(value: &str) -> Result<(), SetOriginatorError> { + let originator = build_originator(value.to_string())?; + ORIGINATOR + .set(originator) + .map_err(|_| SetOriginatorError::AlreadyInitialized) +} + +pub fn originator() -> &'static Originator { + ORIGINATOR.get_or_init(init_originator_from_env) +} pub fn get_codex_user_agent() -> String { let build_version = env!("CARGO_PKG_VERSION"); let os_info = os_info::get(); let prefix = format!( "{}/{build_version} ({} {}; {}) {}", - ORIGINATOR.value.as_str(), + originator().value.as_str(), os_info.os_type(), os_info.version(), os_info.architecture().unwrap_or("unknown"), @@ -100,7 +128,7 @@ fn sanitize_user_agent(candidate: String, fallback: &str) -> String { tracing::warn!( "Falling back to default Codex originator because base user agent string is invalid" ); - ORIGINATOR.value.clone() + originator().value.clone() } } @@ -109,7 +137,7 @@ pub fn create_client() -> reqwest::Client { use reqwest::header::HeaderMap; let mut headers = HeaderMap::new(); - headers.insert("originator", ORIGINATOR.header_value.clone()); + headers.insert("originator", originator().header_value.clone()); let ua = get_codex_user_agent(); let mut builder = reqwest::Client::builder() diff --git a/codex-rs/core/src/otel_init.rs b/codex-rs/core/src/otel_init.rs index 0d6002c225..e7175edc7f 100644 --- a/codex-rs/core/src/otel_init.rs +++ b/codex-rs/core/src/otel_init.rs @@ -1,7 +1,7 @@ use crate::config::Config; use crate::config_types::OtelExporterKind as Kind; use crate::config_types::OtelHttpProtocol as Protocol; -use crate::default_client::ORIGINATOR; +use crate::default_client::originator; use codex_otel::config::OtelExporter; use codex_otel::config::OtelHttpProtocol; use codex_otel::config::OtelSettings; @@ -46,7 +46,7 @@ pub fn build_provider( }; OtelProvider::from(&OtelSettings { - service_name: ORIGINATOR.value.to_owned(), + service_name: originator().value.to_owned(), service_version: service_version.to_string(), codex_home: config.codex_home.clone(), environment: config.otel.environment.to_string(), diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index 23e645413f..6d99dd1357 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -24,7 +24,7 @@ use super::list::Cursor; use super::list::get_conversations; use super::policy::is_persisted_response_item; use crate::config::Config; -use crate::default_client::ORIGINATOR; +use crate::default_client::originator; use crate::git_info::collect_git_info; use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::ResumedHistory; @@ -124,7 +124,7 @@ impl RolloutRecorder { id: session_id, timestamp, cwd: config.cwd.clone(), - originator: ORIGINATOR.value.clone(), + originator: originator().value.clone(), cli_version: env!("CARGO_PKG_VERSION").to_string(), instructions, }), diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index de468493ef..bdf0365026 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -38,9 +38,14 @@ use tracing_subscriber::prelude::*; use crate::cli::Command as ExecCommand; use crate::event_processor::CodexStatus; use crate::event_processor::EventProcessor; +use codex_core::default_client::set_default_originator; use codex_core::find_conversation_path_by_id_str; pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> anyhow::Result<()> { + if let Err(err) = set_default_originator("codex_exec") { + tracing::warn!(?err, "Failed to set codex exec originator override {err:?}"); + } + let Cli { command, images, diff --git a/codex-rs/login/src/server.rs b/codex-rs/login/src/server.rs index 7df9038bc5..275e98901f 100644 --- a/codex-rs/login/src/server.rs +++ b/codex-rs/login/src/server.rs @@ -16,7 +16,7 @@ use base64::Engine; use chrono::Utc; use codex_core::auth::AuthDotJson; use codex_core::auth::get_auth_file; -use codex_core::default_client::ORIGINATOR; +use codex_core::default_client::originator; use codex_core::token_data::TokenData; use codex_core::token_data::parse_id_token; use rand::RngCore; @@ -315,7 +315,7 @@ fn build_authorize_url( ("id_token_add_organizations", "true"), ("codex_cli_simplified_flow", "true"), ("state", state), - ("originator", ORIGINATOR.value.as_str()), + ("originator", originator().value.as_str()), ]; let qs = query .into_iter() From 27c6c5d7a76fffa0b712086a84c8479fea79f86b Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 29 Sep 2025 21:15:02 -0700 Subject: [PATCH 19/23] SDK CI (#4483) Build debug codex in SDK configuration --- .github/workflows/sdk.yml | 43 + pnpm-lock.yaml | 1440 +------------------------------ sdk/typescript/.eslintrc.json | 41 - sdk/typescript/eslint.config.js | 8 + sdk/typescript/package.json | 12 +- 5 files changed, 90 insertions(+), 1454 deletions(-) create mode 100644 .github/workflows/sdk.yml delete mode 100644 sdk/typescript/.eslintrc.json create mode 100644 sdk/typescript/eslint.config.js diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml new file mode 100644 index 0000000000..0f3a7a194b --- /dev/null +++ b/.github/workflows/sdk.yml @@ -0,0 +1,43 @@ +name: sdk + +on: + push: + branches: [main] + pull_request: {} + +jobs: + sdks: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: pnpm + + - uses: dtolnay/rust-toolchain@1.90 + + - name: build codex + run: cargo build --bin codex + working-directory: codex-rs + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build SDK packages + run: pnpm -r --filter ./sdk/typescript run build + + - name: Lint SDK packages + run: pnpm -r --filter ./sdk/typescript run lint + + - name: Test SDK packages + run: pnpm -r --filter ./sdk/typescript run test diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbd7c50b32..5d65067a3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,24 +25,15 @@ importers: '@types/node': specifier: ^20.19.18 version: 20.19.18 - '@typescript-eslint/eslint-plugin': - specifier: ^8.45.0 - version: 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2) - '@typescript-eslint/parser': - specifier: ^8.45.0 - version: 8.45.0(eslint@9.36.0)(typescript@5.9.2) eslint: specifier: ^9.36.0 version: 9.36.0 eslint-config-prettier: specifier: ^9.1.2 version: 9.1.2(eslint@9.36.0) - eslint-import-resolver-typescript: - specifier: ^3.10.1 - version: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0) - eslint-plugin-import: - specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) + eslint-plugin-jest: + specifier: ^29.0.1 + version: 29.0.1(@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) @@ -61,6 +52,9 @@ importers: typescript: specifier: ^5.9.2 version: 5.9.2 + typescript-eslint: + specifier: ^8.45.0 + version: 8.45.0(eslint@9.36.0)(typescript@5.9.2) packages: @@ -233,15 +227,6 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@emnapi/core@1.5.0': - resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} - - '@emnapi/runtime@1.5.0': - resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} - - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - '@esbuild/aix-ppc64@0.25.10': resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} engines: {node: '>=18'} @@ -549,9 +534,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -564,10 +546,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nolyfill/is-core-module@1.0.39': - resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} - engines: {node: '>=12.4.0'} - '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -682,9 +660,6 @@ packages: cpu: [x64] os: [win32] - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -706,9 +681,6 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -742,9 +714,6 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@20.19.18': resolution: {integrity: sha512-KeYVbfnbsBCyKG8e3gmUqAfyZNcoj/qpEbHRkQkfZdKOBrU7QQ+BsTdfqLSWX9/m1ytYreMhpKvp+EZi3UFYAg==} @@ -816,101 +785,6 @@ packages: resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} - cpu: [arm] - os: [android] - - '@unrs/resolver-binding-android-arm64@1.11.1': - resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} - cpu: [arm64] - os: [android] - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} - cpu: [arm64] - os: [darwin] - - '@unrs/resolver-binding-darwin-x64@1.11.1': - resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} - cpu: [x64] - os: [darwin] - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} - cpu: [x64] - os: [freebsd] - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} - cpu: [arm64] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} - cpu: [arm64] - os: [linux] - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} - cpu: [ppc64] - os: [linux] - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} - cpu: [riscv64] - os: [linux] - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} - cpu: [riscv64] - os: [linux] - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} - cpu: [s390x] - os: [linux] - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} - cpu: [x64] - os: [linux] - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} - cpu: [x64] - os: [linux] - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} - cpu: [arm64] - os: [win32] - - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} - cpu: [ia32] - os: [win32] - - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} - cpu: [x64] - os: [win32] - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -968,38 +842,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} - engines: {node: '>= 0.4'} - - array.prototype.findlastindex@1.2.6: - resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} - engines: {node: '>= 0.4'} - - array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1067,18 +909,6 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1160,26 +990,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1204,14 +1014,6 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -1224,14 +1026,6 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1251,34 +1045,6 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} - engines: {node: '>= 0.4'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} - engines: {node: '>= 0.4'} - esbuild@0.25.10: resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} engines: {node: '>=18'} @@ -1302,51 +1068,17 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - - eslint-import-resolver-typescript@3.10.1: - resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} - engines: {node: ^14.18.0 || >=16.0.0} + eslint-plugin-jest@29.0.1: + resolution: {integrity: sha512-EE44T0OSMCeXhDrrdsbKAhprobKkPtJTbQz5yEktysNpHeDZTAL1SfDTNKmcFfJkY6yrQLtTKZALrD3j/Gpmiw==} + engines: {node: ^20.12.0 || ^22.0.0 || >=24.0.0} peerDependencies: - eslint: '*' - eslint-plugin-import: '*' - eslint-plugin-import-x: '*' + '@typescript-eslint/eslint-plugin': ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 + jest: '*' peerDependenciesMeta: - eslint-plugin-import: + '@typescript-eslint/eslint-plugin': optional: true - eslint-plugin-import-x: - optional: true - - eslint-module-utils@2.12.1: - resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - - eslint-plugin-import@2.32.0: - resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - peerDependenciesMeta: - '@typescript-eslint/parser': + jest: optional: true eslint-scope@8.4.0: @@ -1462,10 +1194,6 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -1481,17 +1209,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} - engines: {node: '>= 0.4'} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - - generator-function@2.0.0: - resolution: {integrity: sha512-xPypGGincdfyl/AiSGa7GjXLkvld9V7GjZlowup9SHIJnQnHLFiLODCd/DqKOp0PBagbHJ68r1KJI9Mut7m4sA==} - engines: {node: '>= 0.4'} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1500,29 +1217,14 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - - get-tsconfig@4.10.1: - resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1543,14 +1245,6 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1562,29 +1256,10 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -1624,56 +1299,17 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - - is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - - is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - - is-bun-module@2.0.0: - resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} - is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - - is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -1682,73 +1318,18 @@ packages: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} - is-generator-function@1.1.1: - resolution: {integrity: sha512-Gn8BWUdrTzf9XUJAvqIYP7QnSC3mKs8QjQdGdJ7HmBemzZo14wj/OVmmAwgxDX/7WhFEjboybL4VhXGIQYPlOA==} - engines: {node: '>= 0.4'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - - is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} - - is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} - is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - - is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - - is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1940,10 +1521,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -2011,10 +1588,6 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2058,11 +1631,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-postinstall@0.3.3: - resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - hasBin: true - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -2087,30 +1655,6 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2122,10 +1666,6 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} - p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -2201,10 +1741,6 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -2266,14 +1802,6 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2290,9 +1818,6 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve.exports@2.0.3: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} @@ -2314,35 +1839,11 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} - engines: {node: '>=0.4'} - - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} hasBin: true - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2351,22 +1852,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -2400,17 +1885,10 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - stable-hash@0.0.5: - resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} - stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} - string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -2423,18 +1901,6 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} - engines: {node: '>= 0.4'} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2443,10 +1909,6 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} @@ -2558,12 +2020,6 @@ packages: '@swc/wasm': optional: true - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsup@8.5.0: resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} engines: {node: '>=18'} @@ -2599,21 +2055,12 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} - engines: {node: '>= 0.4'} + typescript-eslint@8.45.0: + resolution: {integrity: sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} @@ -2628,16 +2075,9 @@ packages: engines: {node: '>=0.8.0'} hasBin: true - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -2663,22 +2103,6 @@ packages: whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} - engines: {node: '>= 0.4'} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2929,22 +2353,6 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@emnapi/core@1.5.0': - dependencies: - '@emnapi/wasi-threads': 1.1.0 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.5.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.1.0': - dependencies: - tslib: 2.8.1 - optional: true - '@esbuild/aix-ppc64@0.25.10': optional: true @@ -3283,13 +2691,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@napi-rs/wasm-runtime@0.2.12': - dependencies: - '@emnapi/core': 1.5.0 - '@emnapi/runtime': 1.5.0 - '@tybys/wasm-util': 0.10.1 - optional: true - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3302,8 +2703,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nolyfill/is-core-module@1.0.39': {} - '@pkgjs/parseargs@0.11.0': optional: true @@ -3373,8 +2772,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.3': optional: true - '@rtsao/scc@1.1.0': {} - '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -3393,11 +2790,6 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.4 @@ -3442,8 +2834,6 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/json5@0.0.29': {} - '@types/node@20.19.18': dependencies: undici-types: 6.21.0 @@ -3549,65 +2939,6 @@ snapshots: '@typescript-eslint/types': 8.45.0 eslint-visitor-keys: 4.2.1 - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - optional: true - - '@unrs/resolver-binding-android-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - dependencies: - '@napi-rs/wasm-runtime': 0.2.12 - optional: true - - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - optional: true - - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - optional: true - - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - optional: true - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -3656,62 +2987,6 @@ snapshots: argparse@2.0.1: {} - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-includes@3.1.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - - array.prototype.findlastindex@1.2.6: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.1.0 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - async-function@1.0.0: {} - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - babel-jest@29.7.0(@babel/core@7.28.4): dependencies: '@babel/core': 7.28.4 @@ -3809,23 +3084,6 @@ snapshots: cac@6.7.14: {} - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bind@1.0.8: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - callsites@3.1.0: {} camelcase@5.3.1: {} @@ -3898,28 +3156,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - debug@3.2.7: - dependencies: - ms: 2.1.3 - debug@4.4.3: dependencies: ms: 2.1.3 @@ -3930,34 +3166,12 @@ snapshots: deepmerge@4.3.1: {} - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - detect-newline@3.1.0: {} diff-sequences@29.6.3: {} diff@4.0.2: {} - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - eastasianwidth@0.2.0: {} electron-to-chromium@1.5.227: {} @@ -3972,88 +3186,6 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-abstract@1.24.0: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-negative-zero: 2.0.3 - is-regex: 1.2.1 - is-set: 2.0.3 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.19 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.2 - - es-to-primitive@1.3.0: - dependencies: - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - esbuild@0.25.10: optionalDependencies: '@esbuild/aix-ppc64': 0.25.10 @@ -4093,68 +3225,16 @@ snapshots: dependencies: eslint: 9.36.0 - eslint-import-resolver-node@0.3.9: + eslint-plugin-jest@29.0.1(@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2): dependencies: - debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 1.22.10 - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3 + '@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2) eslint: 9.36.0 - get-tsconfig: 4.10.1 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.15 - unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) + '@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2) + jest: 29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) transitivePeerDependencies: - supports-color - - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.45.0(eslint@9.36.0)(typescript@5.9.2) - eslint: 9.36.0 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0) - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 9.36.0 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 7.7.2 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.45.0(eslint@9.36.0)(typescript@5.9.2) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color + - typescript eslint-scope@8.4.0: dependencies: @@ -4304,10 +3384,6 @@ snapshots: flatted@3.3.3: {} - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -4320,55 +3396,14 @@ snapshots: function-bind@1.1.2: {} - function.prototype.name@1.1.8: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - functions-have-names: 1.2.3 - hasown: 2.0.2 - is-callable: 1.2.7 - - functions-have-names@1.2.3: {} - - generator-function@2.0.0: {} - gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - get-package-type@0.1.0: {} - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - get-stream@6.0.1: {} - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - - get-tsconfig@4.10.1: - dependencies: - resolve-pkg-maps: 1.0.0 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -4397,13 +3432,6 @@ snapshots: globals@14.0.0: {} - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - - gopd@1.2.0: {} - graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -4417,24 +3445,8 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - has-bigints@1.1.0: {} - has-flag@4.0.0: {} - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -4466,134 +3478,26 @@ snapshots: inherits@2.0.4: {} - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-arrayish@0.2.1: {} - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-bun-module@2.0.0: - dependencies: - semver: 7.7.2 - - is-callable@1.2.7: {} - is-core-module@2.16.1: dependencies: hasown: 2.0.2 - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-extglob@2.1.1: {} - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - is-fullwidth-code-point@3.0.0: {} is-generator-fn@2.1.0: {} - is-generator-function@1.1.1: - dependencies: - call-bound: 1.0.4 - generator-function: 2.0.0 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - is-map@2.0.3: {} - - is-negative-zero@2.0.3: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-number@7.0.0: {} - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - is-stream@2.0.1: {} - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.19 - - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - isarray@2.0.5: {} - isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -4975,10 +3879,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json5@1.0.2: - dependencies: - minimist: 1.2.8 - json5@2.2.3: {} keyv@4.5.4: @@ -5034,8 +3934,6 @@ snapshots: dependencies: tmpl: 1.0.5 - math-intrinsics@1.1.0: {} - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -5077,8 +3975,6 @@ snapshots: nanoid@3.3.11: optional: true - napi-postinstall@0.3.3: {} - natural-compare@1.4.0: {} neo-async@2.6.2: {} @@ -5095,39 +3991,6 @@ snapshots: object-assign@4.1.1: {} - object-inspect@1.13.4: {} - - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -5145,12 +4008,6 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -5215,8 +4072,6 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 - possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(postcss@8.5.6)(yaml@2.8.1): dependencies: lilconfig: 3.1.3 @@ -5258,26 +4113,6 @@ snapshots: readdirp@4.1.2: {} - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - require-directory@2.1.1: {} resolve-cwd@3.0.0: @@ -5288,8 +4123,6 @@ snapshots: resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} - resolve.exports@2.0.3: {} resolve@1.22.10: @@ -5332,83 +4165,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - safe-array-concat@1.1.3: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - semver@7.7.2: {} - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -5433,17 +4197,10 @@ snapshots: sprintf-js@1.0.3: {} - stable-hash@0.0.5: {} - stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 - string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -5461,29 +4218,6 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 - string.prototype.trim@1.2.10: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - has-property-descriptors: 1.0.2 - - string.prototype.trimend@1.0.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -5492,8 +4226,6 @@ snapshots: dependencies: ansi-regex: 6.2.2 - strip-bom@3.0.0: {} - strip-bom@4.0.0: {} strip-final-newline@2.0.0: {} @@ -5598,16 +4330,6 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - - tslib@2.8.1: - optional: true - tsup@8.5.0(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1): dependencies: bundle-require: 5.1.0(esbuild@0.25.10) @@ -5646,38 +4368,16 @@ snapshots: type-fest@4.41.0: {} - typed-array-buffer@1.0.3: + typescript-eslint@8.45.0(eslint@9.36.0)(typescript@5.9.2): dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.7: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 + '@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2) + eslint: 9.36.0 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color typescript@5.9.2: {} @@ -5686,39 +4386,8 @@ snapshots: uglify-js@3.19.3: optional: true - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - undici-types@6.21.0: {} - unrs-resolver@1.11.1: - dependencies: - napi-postinstall: 0.3.3 - optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.11.1 - '@unrs/resolver-binding-android-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-x64': 1.11.1 - '@unrs/resolver-binding-freebsd-x64': 1.11.1 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 - '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-musl': 1.11.1 - '@unrs/resolver-binding-wasm32-wasi': 1.11.1 - '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 - '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 - '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - update-browserslist-db@1.1.3(browserslist@4.26.2): dependencies: browserslist: 4.26.2 @@ -5749,47 +4418,6 @@ snapshots: tr46: 1.0.1 webidl-conversions: 4.0.2 - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.1.8 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.1 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.19 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.19: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/sdk/typescript/.eslintrc.json b/sdk/typescript/.eslintrc.json deleted file mode 100644 index 1cae2c818d..0000000000 --- a/sdk/typescript/.eslintrc.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "root": true, - "env": { - "node": true, - "es2022": true - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json", - "sourceType": "module" - }, - "plugins": ["@typescript-eslint", "import"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - "prettier" - ], - "settings": { - "import/resolver": { - "typescript": {} - } - }, - "rules": { - "import/order": [ - "error", - { - "newlines-between": "always", - "alphabetize": { "order": "asc", "caseInsensitive": true } - } - ], - "@typescript-eslint/ban-ts-comment": [ - "error", - { - "ts-expect-error": "allow-with-description" - } - ], - "func-style": ["error", "declaration"] - } -} diff --git a/sdk/typescript/eslint.config.js b/sdk/typescript/eslint.config.js new file mode 100644 index 0000000000..c2fb27d2df --- /dev/null +++ b/sdk/typescript/eslint.config.js @@ -0,0 +1,8 @@ +import eslint from '@eslint/js'; +import { defineConfig } from 'eslint/config'; +import tseslint from 'typescript-eslint'; + +export default defineConfig( + eslint.configs.recommended, + tseslint.configs.recommended, +); diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index a09de87c96..b68f939c12 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -30,8 +30,8 @@ "clean": "rm -rf dist", "build": "tsup", "build:watch": "tsup --watch", - "lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\"", - "lint:fix": "eslint --fix \"src/**/*.ts\" \"tests/**/*.ts\"", + "lint": "pnpm eslint \"src/**/*.ts\" \"tests/**/*.ts\"", + "lint:fix": "pnpm eslint --fix \"src/**/*.ts\" \"tests/**/*.ts\"", "test": "jest", "test:watch": "jest --watch", "coverage": "jest --coverage", @@ -42,17 +42,15 @@ "devDependencies": { "@types/jest": "^29.5.14", "@types/node": "^20.19.18", - "@typescript-eslint/eslint-plugin": "^8.45.0", - "@typescript-eslint/parser": "^8.45.0", + "typescript-eslint": "^8.45.0", "eslint": "^9.36.0", "eslint-config-prettier": "^9.1.2", - "eslint-import-resolver-typescript": "^3.10.1", - "eslint-plugin-import": "^2.32.0", "jest": "^29.7.0", "prettier": "^3.6.2", "ts-jest": "^29.3.4", "ts-node": "^10.9.2", "tsup": "^8.5.0", - "typescript": "^5.9.2" + "typescript": "^5.9.2", + "eslint-plugin-jest": "^29.0.1" } } From 87a654cf6bf7adaa1d3e785442e7629db8a8205f Mon Sep 17 00:00:00 2001 From: dedrisian-oai Date: Mon, 29 Sep 2025 23:03:37 -0700 Subject: [PATCH 20/23] Move PR-style review to top (#4486) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2025-09-29 at 10 31 22 PM --- codex-rs/tui/src/chatwidget.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 9a36addb0a..981e0dfa94 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1778,6 +1778,20 @@ impl ChatWidget { pub(crate) fn open_review_popup(&mut self) { let mut items: Vec = Vec::new(); + items.push(SelectionItem { + name: "Review against a base branch".to_string(), + description: Some("(PR Style)".into()), + is_current: false, + actions: vec![Box::new({ + let cwd = self.config.cwd.clone(); + move |tx| { + tx.send(AppEvent::OpenReviewBranchPicker(cwd.clone())); + } + })], + dismiss_on_select: false, + search_value: None, + }); + items.push(SelectionItem { name: "Review uncommitted changes".to_string(), description: None, @@ -1811,20 +1825,6 @@ impl ChatWidget { search_value: None, }); - items.push(SelectionItem { - name: "Review against a base branch".to_string(), - description: None, - is_current: false, - actions: vec![Box::new({ - let cwd = self.config.cwd.clone(); - move |tx| { - tx.send(AppEvent::OpenReviewBranchPicker(cwd.clone())); - } - })], - dismiss_on_select: false, - search_value: None, - }); - items.push(SelectionItem { name: "Custom review instructions".to_string(), description: None, From 2e95e5602d5736ea5b3a41e89c20f9db0b820282 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Mon, 29 Sep 2025 23:44:16 -0700 Subject: [PATCH 21/23] Update MCP docs to reference experimental RMCP client (#4422) --- docs/config.md | 63 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/docs/config.md b/docs/config.md index e0d4e66c7c..c8ebf94bdd 100644 --- a/docs/config.md +++ b/docs/config.md @@ -336,46 +336,59 @@ Codex provides three main Approval Presets: You can further customize how Codex runs at the command line using the `--ask-for-approval` and `--sandbox` options. -## mcp_servers +## MCP Servers -Defines the list of MCP servers that Codex can consult for tool use. Currently, only servers that are launched by executing a program that communicate over stdio are supported. For servers that use the SSE transport, consider an adapter like [mcp-proxy](https://github.com/sparfenyuk/mcp-proxy). +You can configure Codex to use [MCP servers](https://modelcontextprotocol.io/about) to give Codex access to external applications, resources, or services such as [Playwright](https://github.com/microsoft/playwright-mcp), [Figma](https://www.figma.com/blog/design-context-everywhere-you-build/), [documentation](https://context7.com/), and [more](https://github.com/mcp?utm_source=blog-source&utm_campaign=mcp-registry-server-launch-2025). -**Note:** Codex may cache the list of tools and resources from an MCP server so that Codex can include this information in context at startup without spawning all the servers. This is designed to save resources by loading MCP servers lazily. +### Server transport configuration -Each server may set `startup_timeout_sec` to adjust how long Codex waits for it to start and respond to a tools listing. The default is `10` seconds. -Similarly, `tool_timeout_sec` limits how long individual tool calls may run (default: `60` seconds), and Codex will fall back to the default when this value is omitted. - -This config option is comparable to how Claude and Cursor define `mcpServers` in their respective JSON config files, though because Codex uses TOML for its config language, the format is slightly different. For example, the following config in JSON: - -```json -{ - "mcpServers": { - "server-name": { - "command": "npx", - "args": ["-y", "mcp-server"], - "env": { - "API_KEY": "value" - } - } - } -} -``` - -Should be represented as follows in `~/.codex/config.toml`: +#### STDIO ```toml -# IMPORTANT: the top-level key is `mcp_servers` rather than `mcpServers`. +# The top-level table name must be `mcp_servers` +# The sub-table name (`server-name` in this example) can be anything you would like. [mcp_servers.server-name] command = "npx" +# Optional args = ["-y", "mcp-server"] +# Optional: propagate additional env vars to the MVP server. +# A default whitelist of env vars will be propagated to the MCP server. +# https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/utils.rs#L82 env = { "API_KEY" = "value" } +``` + +#### Streamable HTTP + +```toml +# Streamable HTTP requires the experimental rmcp client +experimental_use_rmcp_client = true +[mcp_servers.figma] +url = "http://127.0.0.1:3845/mcp" +# Optional bearer token to be passed into an `Authorization: Bearer ` header +# Use this with caution because the token is in plaintext. +bearer_token = "" +``` + +### Other configuration options + +```toml # Optional: override the default 10s startup timeout startup_timeout_sec = 20 # Optional: override the default 60s per-tool timeout tool_timeout_sec = 30 ``` -You can also manage these entries from the CLI [experimental]: +### Experimental RMCP client + +Codex is transitioning to the [official Rust MCP SDK](https://github.com/modelcontextprotocol/rust-sdk) and new functionality such as streamable http servers will only work with the new client. + +Please try and report issues with the new client. To enable it, add this to the top level of your `config.toml` + +```toml +experimental_use_rmcp_client = true +``` + +### MCP CLI commands ```shell # Add a server (env can be repeated; `--` separates the launcher command) From d9dbf4882879577ae8a9d81946994b325e359ed7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 30 Sep 2025 00:06:18 -0700 Subject: [PATCH 22/23] fix: separate `codex mcp` into `codex mcp-server` and `codex app-server` (#4471) This is a very large PR with some non-backwards-compatible changes. Historically, `codex mcp` (or `codex mcp serve`) started a JSON-RPC-ish server that had two overlapping responsibilities: - Running an MCP server, providing some basic tool calls. - Running the app server used to power experiences such as the VS Code extension. This PR aims to separate these into distinct concepts: - `codex mcp-server` for the MCP server - `codex app-server` for the "application server" Note `codex mcp` still exists because it already has its own subcommands for MCP management (`list`, `add`, etc.) The MCP logic continues to live in `codex-rs/mcp-server` whereas the refactored app server logic is in the new `codex-rs/app-server` folder. Note that most of the existing integration tests in `codex-rs/mcp-server/tests/suite` were actually for the app server, so all the tests have been moved with the exception of `codex-rs/mcp-server/tests/suite/mod.rs`. Because this is already a large diff, I tried not to change more than I had to, so `codex-rs/app-server/tests/common/mcp_process.rs` still uses the name `McpProcess` for now, but I will do some mechanical renamings to things like `AppServer` in subsequent PRs. While `mcp-server` and `app-server` share some overlapping functionality (like reading streams of JSONL and dispatching based on message types) and some differences (completely different message types), I ended up doing a bit of copypasta between the two crates, as both have somewhat similar `message_processor.rs` and `outgoing_message.rs` files for now, though I expect them to diverge more in the near future. One material change is that of the initialize handshake for `codex app-server`, as we no longer use the MCP types for that handshake. Instead, we update `codex-rs/protocol/src/mcp_protocol.rs` to add an `Initialize` variant to `ClientRequest`, which takes the `ClientInfo` object we need to update the `USER_AGENT_SUFFIX` in `codex-rs/app-server/src/message_processor.rs`. One other material change is in `codex-rs/app-server/src/codex_message_processor.rs` where I eliminated a use of the `send_event_as_notification()` method I am generally trying to deprecate (because it blindly maps an `EventMsg` into a `JSONNotification`) in favor of `send_server_notification()`, which takes a `ServerNotification`, as that is intended to be a custom enum of all notification types supported by the app server. So to make this update, I had to introduce a new variant of `ServerNotification`, `SessionConfigured`, which is a non-backwards compatible change with the old `codex mcp`, and clients will have to be updated after the next release that contains this PR. Note that `codex-rs/app-server/tests/suite/list_resume.rs` also had to be update to reflect this change. I introduced `codex-rs/utils/json-to-toml/src/lib.rs` as a small utility crate to avoid some of the copying between `mcp-server` and `app-server`. --- codex-rs/Cargo.lock | 61 ++- codex-rs/Cargo.toml | 13 +- codex-rs/README.md | 6 +- codex-rs/app-server/Cargo.toml | 51 ++ .../src/codex_message_processor.rs | 24 +- codex-rs/app-server/src/error_code.rs | 2 + .../src/fuzzy_file_search.rs | 0 codex-rs/app-server/src/lib.rs | 139 +++++ codex-rs/app-server/src/main.rs | 10 + codex-rs/app-server/src/message_processor.rs | 133 +++++ codex-rs/app-server/src/outgoing_message.rs | 239 +++++++++ codex-rs/app-server/tests/all.rs | 3 + codex-rs/app-server/tests/common/Cargo.toml | 22 + codex-rs/app-server/tests/common/lib.rs | 17 + .../app-server/tests/common/mcp_process.rs | 477 ++++++++++++++++++ .../tests/common/mock_model_server.rs | 47 ++ codex-rs/app-server/tests/common/responses.rs | 95 ++++ .../tests/suite/archive_conversation.rs | 4 +- .../tests/suite/auth.rs | 4 +- .../suite/codex_message_processor_flow.rs | 10 +- .../tests/suite/config.rs | 4 +- .../tests/suite/create_conversation.rs | 8 +- .../tests/suite/fuzzy_file_search.rs | 2 +- .../tests/suite/interrupt.rs | 10 +- .../tests/suite/list_resume.rs | 37 +- .../tests/suite/login.rs | 4 +- codex-rs/app-server/tests/suite/mod.rs | 13 + .../tests/suite/send_message.rs | 8 +- .../tests/suite/set_default_model.rs | 4 +- .../tests/suite/user_agent.rs | 6 +- .../tests/suite/user_info.rs | 4 +- codex-rs/cli/Cargo.toml | 1 + codex-rs/cli/src/main.rs | 14 +- codex-rs/cli/src/mcp_cmd.rs | 14 +- codex-rs/docs/codex_mcp_interface.md | 8 +- codex-rs/mcp-server/Cargo.toml | 6 +- codex-rs/mcp-server/src/codex_tool_config.rs | 3 +- codex-rs/mcp-server/src/lib.rs | 3 - codex-rs/mcp-server/src/message_processor.rs | 24 +- codex-rs/mcp-server/src/outgoing_message.rs | 52 -- codex-rs/mcp-server/tests/common/Cargo.toml | 1 - .../mcp-server/tests/common/mcp_process.rs | 226 --------- codex-rs/mcp-server/tests/suite/mod.rs | 14 - codex-rs/protocol-ts/src/lib.rs | 2 + codex-rs/protocol/README.md | 2 +- codex-rs/protocol/src/mcp_protocol.rs | 90 +++- codex-rs/utils/json-to-toml/Cargo.toml | 14 + .../json-to-toml/src/lib.rs} | 2 +- docs/advanced.md | 6 +- 49 files changed, 1525 insertions(+), 414 deletions(-) create mode 100644 codex-rs/app-server/Cargo.toml rename codex-rs/{mcp-server => app-server}/src/codex_message_processor.rs (98%) create mode 100644 codex-rs/app-server/src/error_code.rs rename codex-rs/{mcp-server => app-server}/src/fuzzy_file_search.rs (100%) create mode 100644 codex-rs/app-server/src/lib.rs create mode 100644 codex-rs/app-server/src/main.rs create mode 100644 codex-rs/app-server/src/message_processor.rs create mode 100644 codex-rs/app-server/src/outgoing_message.rs create mode 100644 codex-rs/app-server/tests/all.rs create mode 100644 codex-rs/app-server/tests/common/Cargo.toml create mode 100644 codex-rs/app-server/tests/common/lib.rs create mode 100644 codex-rs/app-server/tests/common/mcp_process.rs create mode 100644 codex-rs/app-server/tests/common/mock_model_server.rs create mode 100644 codex-rs/app-server/tests/common/responses.rs rename codex-rs/{mcp-server => app-server}/tests/suite/archive_conversation.rs (98%) rename codex-rs/{mcp-server => app-server}/tests/suite/auth.rs (99%) rename codex-rs/{mcp-server => app-server}/tests/suite/codex_message_processor_flow.rs (98%) rename codex-rs/{mcp-server => app-server}/tests/suite/config.rs (98%) rename codex-rs/{mcp-server => app-server}/tests/suite/create_conversation.rs (96%) rename codex-rs/{mcp-server => app-server}/tests/suite/fuzzy_file_search.rs (99%) rename codex-rs/{mcp-server => app-server}/tests/suite/interrupt.rs (95%) rename codex-rs/{mcp-server => app-server}/tests/suite/list_resume.rs (87%) rename codex-rs/{mcp-server => app-server}/tests/suite/login.rs (98%) create mode 100644 codex-rs/app-server/tests/suite/mod.rs rename codex-rs/{mcp-server => app-server}/tests/suite/send_message.rs (97%) rename codex-rs/{mcp-server => app-server}/tests/suite/set_default_model.rs (97%) rename codex-rs/{mcp-server => app-server}/tests/suite/user_agent.rs (91%) rename codex-rs/{mcp-server => app-server}/tests/suite/user_info.rs (97%) create mode 100644 codex-rs/utils/json-to-toml/Cargo.toml rename codex-rs/{mcp-server/src/json_to_toml.rs => utils/json-to-toml/src/lib.rs} (97%) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index c8f362aa03..6707e75bbb 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -165,6 +165,20 @@ version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +[[package]] +name = "app_test_support" +version = "0.0.0" +dependencies = [ + "anyhow", + "assert_cmd", + "codex-protocol", + "mcp-types", + "serde", + "serde_json", + "tokio", + "wiremock", +] + [[package]] name = "arboard" version = "3.6.0" @@ -636,6 +650,36 @@ dependencies = [ "tracing", ] +[[package]] +name = "codex-app-server" +version = "0.0.0" +dependencies = [ + "anyhow", + "app_test_support", + "assert_cmd", + "base64", + "codex-arg0", + "codex-common", + "codex-core", + "codex-file-search", + "codex-login", + "codex-protocol", + "codex-utils-json-to-toml", + "core_test_support", + "mcp-types", + "os_info", + "pretty_assertions", + "serde", + "serde_json", + "tempfile", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "uuid", + "wiremock", +] + [[package]] name = "codex-apply-patch" version = "0.0.0" @@ -685,6 +729,7 @@ dependencies = [ "assert_cmd", "clap", "clap_complete", + "codex-app-server", "codex-arg0", "codex-chatgpt", "codex-common", @@ -914,13 +959,11 @@ version = "0.0.0" dependencies = [ "anyhow", "assert_cmd", - "base64", "codex-arg0", "codex-common", "codex-core", - "codex-file-search", - "codex-login", "codex-protocol", + "codex-utils-json-to-toml", "core_test_support", "mcp-types", "mcp_test_support", @@ -932,10 +975,8 @@ dependencies = [ "shlex", "tempfile", "tokio", - "toml", "tracing", "tracing-subscriber", - "uuid", "wiremock", ] @@ -1104,6 +1145,15 @@ dependencies = [ "vt100", ] +[[package]] +name = "codex-utils-json-to-toml" +version = "0.0.0" +dependencies = [ + "pretty_assertions", + "serde_json", + "toml", +] + [[package]] name = "codex-utils-readiness" version = "0.0.0" @@ -3004,7 +3054,6 @@ dependencies = [ "assert_cmd", "codex-core", "codex-mcp-server", - "codex-protocol", "mcp-types", "os_info", "pretty_assertions", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 1c591897ee..7946f1d0e7 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "ansi-escape", + "app-server", "apply-patch", "arg0", "cli", @@ -23,6 +24,7 @@ members = [ "responses-api-proxy", "otel", "tui", + "utils/json-to-toml", "utils/readiness", ] resolver = "2" @@ -37,7 +39,9 @@ edition = "2024" [workspace.dependencies] # Internal +app_test_support = { path = "app-server/tests/common" } codex-ansi-escape = { path = "ansi-escape" } +codex-app-server = { path = "app-server" } codex-apply-patch = { path = "apply-patch" } codex-arg0 = { path = "arg0" } codex-chatgpt = { path = "chatgpt" } @@ -51,12 +55,13 @@ codex-login = { path = "login" } codex-mcp-client = { path = "mcp-client" } codex-mcp-server = { path = "mcp-server" } codex-ollama = { path = "ollama" } +codex-otel = { path = "otel" } codex-process-hardening = { path = "process-hardening" } codex-protocol = { path = "protocol" } codex-protocol-ts = { path = "protocol-ts" } codex-rmcp-client = { path = "rmcp-client" } codex-tui = { path = "tui" } -codex-otel = { path = "otel" } +codex-utils-json-to-toml = { path = "utils/json-to-toml" } codex-utils-readiness = { path = "utils/readiness" } core_test_support = { path = "core/tests/common" } mcp-types = { path = "mcp-types" } @@ -106,10 +111,10 @@ multimap = "0.10.0" nucleo-matcher = "0.3.1" openssl-sys = "*" opentelemetry = "0.30.0" -opentelemetry_sdk = "0.30.0" -opentelemetry-otlp = "0.30.0" opentelemetry-appender-tracing = "0.30.0" +opentelemetry-otlp = "0.30.0" opentelemetry-semantic-conventions = "0.30.0" +opentelemetry_sdk = "0.30.0" os_info = "3.12.0" owo-colors = "4.2.0" path-absolutize = "3.1.1" @@ -148,6 +153,7 @@ tokio-test = "0.4" tokio-util = "0.7.16" toml = "0.9.5" toml_edit = "0.23.4" +tonic = "0.13.1" tracing = "0.1.41" tracing-appender = "0.2.3" tracing-subscriber = "0.3.20" @@ -155,7 +161,6 @@ tracing-test = "0.2.5" tree-sitter = "0.25.9" tree-sitter-bash = "0.25.0" ts-rs = "11" -tonic = "0.13.1" unicode-segmentation = "1.12.0" unicode-width = "0.2" url = "2" diff --git a/codex-rs/README.md b/codex-rs/README.md index f51f746892..46eda63a1e 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -25,12 +25,14 @@ Codex supports a rich set of configuration options. Note that the Rust CLI uses Codex CLI functions as an MCP client that can connect to MCP servers on startup. See the [`mcp_servers`](../docs/config.md#mcp_servers) section in the configuration documentation for details. -It is still experimental, but you can also launch Codex as an MCP _server_ by running `codex mcp`. Use the [`@modelcontextprotocol/inspector`](https://github.com/modelcontextprotocol/inspector) to try it out: +It is still experimental, but you can also launch Codex as an MCP _server_ by running `codex mcp-server`. Use the [`@modelcontextprotocol/inspector`](https://github.com/modelcontextprotocol/inspector) to try it out: ```shell -npx @modelcontextprotocol/inspector codex mcp +npx @modelcontextprotocol/inspector codex mcp-server ``` +Use `codex mcp` to add/list/get/remove MCP server launchers defined in `config.toml`, and `codex mcp-server` to run the MCP server directly. + ### Notifications You can enable notifications by configuring a script that is run whenever the agent finishes a turn. The [notify documentation](../docs/config.md#notify) includes a detailed example that explains how to get desktop notifications via [terminal-notifier](https://github.com/julienXX/terminal-notifier) on macOS. diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml new file mode 100644 index 0000000000..1cc25ef2d1 --- /dev/null +++ b/codex-rs/app-server/Cargo.toml @@ -0,0 +1,51 @@ +[package] +edition = "2024" +name = "codex-app-server" +version = { workspace = true } + +[[bin]] +name = "codex-app-server" +path = "src/main.rs" + +[lib] +name = "codex_app_server" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +anyhow = { workspace = true } +codex-arg0 = { workspace = true } +codex-common = { workspace = true, features = ["cli"] } +codex-core = { workspace = true } +codex-file-search = { workspace = true } +codex-login = { workspace = true } +codex-protocol = { workspace = true } +codex-utils-json-to-toml = { workspace = true } +# We should only be using mcp-types for JSON-RPC types: it would be nice to +# split this out into a separate crate at some point. +mcp-types = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = [ + "io-std", + "macros", + "process", + "rt-multi-thread", + "signal", +] } +tracing = { workspace = true, features = ["log"] } +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } +uuid = { workspace = true, features = ["serde", "v7"] } + +[dev-dependencies] +app_test_support = { workspace = true } +assert_cmd = { workspace = true } +base64 = { workspace = true } +core_test_support = { workspace = true } +os_info = { workspace = true } +pretty_assertions = { workspace = true } +tempfile = { workspace = true } +toml = { workspace = true } +wiremock = { workspace = true } diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs similarity index 98% rename from codex-rs/mcp-server/src/codex_message_processor.rs rename to codex-rs/app-server/src/codex_message_processor.rs index 53830431fd..4d11aad421 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1,7 +1,6 @@ use crate::error_code::INTERNAL_ERROR_CODE; use crate::error_code::INVALID_REQUEST_ERROR_CODE; use crate::fuzzy_file_search::run_fuzzy_file_search; -use crate::json_to_toml::json_to_toml; use crate::outgoing_message::OutgoingMessageSender; use crate::outgoing_message::OutgoingNotification; use codex_core::AuthManager; @@ -77,6 +76,7 @@ use codex_protocol::mcp_protocol::SendUserMessageResponse; use codex_protocol::mcp_protocol::SendUserTurnParams; use codex_protocol::mcp_protocol::SendUserTurnResponse; use codex_protocol::mcp_protocol::ServerNotification; +use codex_protocol::mcp_protocol::SessionConfiguredNotification; use codex_protocol::mcp_protocol::SetDefaultModelParams; use codex_protocol::mcp_protocol::SetDefaultModelResponse; use codex_protocol::mcp_protocol::UserInfoResponse; @@ -85,6 +85,7 @@ use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::InputMessageKind; use codex_protocol::protocol::USER_MESSAGE_BEGIN; +use codex_utils_json_to_toml::json_to_toml; use mcp_types::JSONRPCErrorError; use mcp_types::RequestId; use std::collections::HashMap; @@ -153,6 +154,9 @@ impl CodexMessageProcessor { pub async fn process_request(&mut self, request: ClientRequest) { match request { + ClientRequest::Initialize { .. } => { + panic!("Initialize should be handled in MessageProcessor"); + } ClientRequest::NewConversation { request_id, params } => { // Do not tokio::spawn() to process new_conversation() // asynchronously because we need to ensure the conversation is @@ -762,11 +766,19 @@ impl CodexMessageProcessor { session_configured, .. }) => { - let event = Event { - id: "".to_string(), - msg: EventMsg::SessionConfigured(session_configured.clone()), - }; - self.outgoing.send_event_as_notification(&event, None).await; + self.outgoing + .send_server_notification(ServerNotification::SessionConfigured( + SessionConfiguredNotification { + session_id: session_configured.session_id, + model: session_configured.model.clone(), + reasoning_effort: session_configured.reasoning_effort, + history_log_id: session_configured.history_log_id, + history_entry_count: session_configured.history_entry_count, + initial_messages: session_configured.initial_messages.clone(), + rollout_path: session_configured.rollout_path.clone(), + }, + )) + .await; let initial_messages = session_configured.initial_messages.map(|msgs| { msgs.into_iter() .filter(|event| { diff --git a/codex-rs/app-server/src/error_code.rs b/codex-rs/app-server/src/error_code.rs new file mode 100644 index 0000000000..1ffd889d40 --- /dev/null +++ b/codex-rs/app-server/src/error_code.rs @@ -0,0 +1,2 @@ +pub(crate) const INVALID_REQUEST_ERROR_CODE: i64 = -32600; +pub(crate) const INTERNAL_ERROR_CODE: i64 = -32603; diff --git a/codex-rs/mcp-server/src/fuzzy_file_search.rs b/codex-rs/app-server/src/fuzzy_file_search.rs similarity index 100% rename from codex-rs/mcp-server/src/fuzzy_file_search.rs rename to codex-rs/app-server/src/fuzzy_file_search.rs diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs new file mode 100644 index 0000000000..a0c40db64a --- /dev/null +++ b/codex-rs/app-server/src/lib.rs @@ -0,0 +1,139 @@ +#![deny(clippy::print_stdout, clippy::print_stderr)] + +use std::io::ErrorKind; +use std::io::Result as IoResult; +use std::path::PathBuf; + +use codex_common::CliConfigOverrides; +use codex_core::config::Config; +use codex_core::config::ConfigOverrides; + +use mcp_types::JSONRPCMessage; +use tokio::io::AsyncBufReadExt; +use tokio::io::AsyncWriteExt; +use tokio::io::BufReader; +use tokio::io::{self}; +use tokio::sync::mpsc; +use tracing::debug; +use tracing::error; +use tracing::info; +use tracing_subscriber::EnvFilter; + +use crate::message_processor::MessageProcessor; +use crate::outgoing_message::OutgoingMessage; +use crate::outgoing_message::OutgoingMessageSender; + +mod codex_message_processor; +mod error_code; +mod fuzzy_file_search; +mod message_processor; +mod outgoing_message; + +/// Size of the bounded channels used to communicate between tasks. The value +/// is a balance between throughput and memory usage – 128 messages should be +/// plenty for an interactive CLI. +const CHANNEL_CAPACITY: usize = 128; + +pub async fn run_main( + codex_linux_sandbox_exe: Option, + cli_config_overrides: CliConfigOverrides, +) -> IoResult<()> { + // Install a simple subscriber so `tracing` output is visible. Users can + // control the log level with `RUST_LOG`. + tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_env_filter(EnvFilter::from_default_env()) + .init(); + + // Set up channels. + let (incoming_tx, mut incoming_rx) = mpsc::channel::(CHANNEL_CAPACITY); + let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded_channel::(); + + // Task: read from stdin, push to `incoming_tx`. + let stdin_reader_handle = tokio::spawn({ + async move { + let stdin = io::stdin(); + let reader = BufReader::new(stdin); + let mut lines = reader.lines(); + + while let Some(line) = lines.next_line().await.unwrap_or_default() { + match serde_json::from_str::(&line) { + Ok(msg) => { + if incoming_tx.send(msg).await.is_err() { + // Receiver gone – nothing left to do. + break; + } + } + Err(e) => error!("Failed to deserialize JSONRPCMessage: {e}"), + } + } + + debug!("stdin reader finished (EOF)"); + } + }); + + // Parse CLI overrides once and derive the base Config eagerly so later + // components do not need to work with raw TOML values. + let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| { + std::io::Error::new( + ErrorKind::InvalidInput, + format!("error parsing -c overrides: {e}"), + ) + })?; + let config = Config::load_with_cli_overrides(cli_kv_overrides, ConfigOverrides::default()) + .map_err(|e| { + std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}")) + })?; + + // Task: process incoming messages. + let processor_handle = tokio::spawn({ + let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx); + let mut processor = MessageProcessor::new( + outgoing_message_sender, + codex_linux_sandbox_exe, + std::sync::Arc::new(config), + ); + async move { + while let Some(msg) = incoming_rx.recv().await { + match msg { + JSONRPCMessage::Request(r) => processor.process_request(r).await, + JSONRPCMessage::Response(r) => processor.process_response(r).await, + JSONRPCMessage::Notification(n) => processor.process_notification(n).await, + JSONRPCMessage::Error(e) => processor.process_error(e), + } + } + + info!("processor task exited (channel closed)"); + } + }); + + // Task: write outgoing messages to stdout. + let stdout_writer_handle = tokio::spawn(async move { + let mut stdout = io::stdout(); + while let Some(outgoing_message) = outgoing_rx.recv().await { + let msg: JSONRPCMessage = outgoing_message.into(); + match serde_json::to_string(&msg) { + Ok(json) => { + if let Err(e) = stdout.write_all(json.as_bytes()).await { + error!("Failed to write to stdout: {e}"); + break; + } + if let Err(e) = stdout.write_all(b"\n").await { + error!("Failed to write newline to stdout: {e}"); + break; + } + } + Err(e) => error!("Failed to serialize JSONRPCMessage: {e}"), + } + } + + info!("stdout writer exited (channel closed)"); + }); + + // Wait for all tasks to finish. The typical exit path is the stdin reader + // hitting EOF which, once it drops `incoming_tx`, propagates shutdown to + // the processor and then to the stdout task. + let _ = tokio::join!(stdin_reader_handle, processor_handle, stdout_writer_handle); + + Ok(()) +} diff --git a/codex-rs/app-server/src/main.rs b/codex-rs/app-server/src/main.rs new file mode 100644 index 0000000000..689ec0877a --- /dev/null +++ b/codex-rs/app-server/src/main.rs @@ -0,0 +1,10 @@ +use codex_app_server::run_main; +use codex_arg0::arg0_dispatch_or_else; +use codex_common::CliConfigOverrides; + +fn main() -> anyhow::Result<()> { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { + run_main(codex_linux_sandbox_exe, CliConfigOverrides::default()).await?; + Ok(()) + }) +} diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs new file mode 100644 index 0000000000..24e0f2320a --- /dev/null +++ b/codex-rs/app-server/src/message_processor.rs @@ -0,0 +1,133 @@ +use std::path::PathBuf; + +use crate::codex_message_processor::CodexMessageProcessor; +use crate::error_code::INVALID_REQUEST_ERROR_CODE; +use crate::outgoing_message::OutgoingMessageSender; +use codex_protocol::mcp_protocol::ClientInfo; +use codex_protocol::mcp_protocol::ClientRequest; +use codex_protocol::mcp_protocol::InitializeResponse; + +use codex_core::AuthManager; +use codex_core::ConversationManager; +use codex_core::config::Config; +use codex_core::default_client::USER_AGENT_SUFFIX; +use codex_core::default_client::get_codex_user_agent; +use mcp_types::JSONRPCError; +use mcp_types::JSONRPCErrorError; +use mcp_types::JSONRPCNotification; +use mcp_types::JSONRPCRequest; +use mcp_types::JSONRPCResponse; +use std::sync::Arc; + +pub(crate) struct MessageProcessor { + outgoing: Arc, + codex_message_processor: CodexMessageProcessor, + initialized: bool, +} + +impl MessageProcessor { + /// Create a new `MessageProcessor`, retaining a handle to the outgoing + /// `Sender` so handlers can enqueue messages to be written to stdout. + pub(crate) fn new( + outgoing: OutgoingMessageSender, + codex_linux_sandbox_exe: Option, + config: Arc, + ) -> Self { + let outgoing = Arc::new(outgoing); + let auth_manager = AuthManager::shared(config.codex_home.clone()); + let conversation_manager = Arc::new(ConversationManager::new(auth_manager.clone())); + let codex_message_processor = CodexMessageProcessor::new( + auth_manager, + conversation_manager, + outgoing.clone(), + codex_linux_sandbox_exe, + config, + ); + + Self { + outgoing, + codex_message_processor, + initialized: false, + } + } + + pub(crate) async fn process_request(&mut self, request: JSONRPCRequest) { + let request_id = request.id.clone(); + if let Ok(request_json) = serde_json::to_value(request) + && let Ok(codex_request) = serde_json::from_value::(request_json) + { + match codex_request { + // Handle Initialize internally so CodexMessageProcessor does not have to concern + // itself with the `initialized` bool. + ClientRequest::Initialize { request_id, params } => { + if self.initialized { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: "Already initialized".to_string(), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + return; + } else { + let ClientInfo { + name, + title: _title, + version, + } = params.client_info; + let user_agent_suffix = format!("{name}; {version}"); + if let Ok(mut suffix) = USER_AGENT_SUFFIX.lock() { + *suffix = Some(user_agent_suffix); + } + + let user_agent = get_codex_user_agent(); + let response = InitializeResponse { user_agent }; + self.outgoing.send_response(request_id, response).await; + + self.initialized = true; + return; + } + } + _ => { + if !self.initialized { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: "Not initialized".to_string(), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + return; + } + } + } + + self.codex_message_processor + .process_request(codex_request) + .await; + } else { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: "Invalid request".to_string(), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + } + } + + pub(crate) async fn process_notification(&self, notification: JSONRPCNotification) { + // Currently, we do not expect to receive any notifications from the + // client, so we just log them. + tracing::info!("<- notification: {:?}", notification); + } + + /// Handle a standalone JSON-RPC response originating from the peer. + pub(crate) async fn process_response(&mut self, response: JSONRPCResponse) { + tracing::info!("<- response: {:?}", response); + let JSONRPCResponse { id, result, .. } = response; + self.outgoing.notify_client_response(id, result).await + } + + /// Handle an error object received from the peer. + pub(crate) fn process_error(&mut self, err: JSONRPCError) { + tracing::error!("<- error: {:?}", err); + } +} diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs new file mode 100644 index 0000000000..25d5e7aa52 --- /dev/null +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -0,0 +1,239 @@ +use std::collections::HashMap; +use std::sync::atomic::AtomicI64; +use std::sync::atomic::Ordering; + +use codex_protocol::mcp_protocol::ServerNotification; +use mcp_types::JSONRPC_VERSION; +use mcp_types::JSONRPCError; +use mcp_types::JSONRPCErrorError; +use mcp_types::JSONRPCMessage; +use mcp_types::JSONRPCNotification; +use mcp_types::JSONRPCRequest; +use mcp_types::JSONRPCResponse; +use mcp_types::RequestId; +use mcp_types::Result; +use serde::Serialize; +use tokio::sync::Mutex; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use tracing::warn; + +use crate::error_code::INTERNAL_ERROR_CODE; + +/// Sends messages to the client and manages request callbacks. +pub(crate) struct OutgoingMessageSender { + next_request_id: AtomicI64, + sender: mpsc::UnboundedSender, + request_id_to_callback: Mutex>>, +} + +impl OutgoingMessageSender { + pub(crate) fn new(sender: mpsc::UnboundedSender) -> Self { + Self { + next_request_id: AtomicI64::new(0), + sender, + request_id_to_callback: Mutex::new(HashMap::new()), + } + } + + pub(crate) async fn send_request( + &self, + method: &str, + params: Option, + ) -> oneshot::Receiver { + let id = RequestId::Integer(self.next_request_id.fetch_add(1, Ordering::Relaxed)); + let outgoing_message_id = id.clone(); + let (tx_approve, rx_approve) = oneshot::channel(); + { + let mut request_id_to_callback = self.request_id_to_callback.lock().await; + request_id_to_callback.insert(id, tx_approve); + } + + let outgoing_message = OutgoingMessage::Request(OutgoingRequest { + id: outgoing_message_id, + method: method.to_string(), + params, + }); + let _ = self.sender.send(outgoing_message); + rx_approve + } + + pub(crate) async fn notify_client_response(&self, id: RequestId, result: Result) { + let entry = { + let mut request_id_to_callback = self.request_id_to_callback.lock().await; + request_id_to_callback.remove_entry(&id) + }; + + match entry { + Some((id, sender)) => { + if let Err(err) = sender.send(result) { + warn!("could not notify callback for {id:?} due to: {err:?}"); + } + } + None => { + warn!("could not find callback for {id:?}"); + } + } + } + + pub(crate) async fn send_response(&self, id: RequestId, response: T) { + match serde_json::to_value(response) { + Ok(result) => { + let outgoing_message = OutgoingMessage::Response(OutgoingResponse { id, result }); + let _ = self.sender.send(outgoing_message); + } + Err(err) => { + self.send_error( + id, + JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("failed to serialize response: {err}"), + data: None, + }, + ) + .await; + } + } + } + + pub(crate) async fn send_server_notification(&self, notification: ServerNotification) { + let _ = self + .sender + .send(OutgoingMessage::AppServerNotification(notification)); + } + + /// All notifications should be migrated to [`ServerNotification`] and + /// [`OutgoingMessage::Notification`] should be removed. + pub(crate) async fn send_notification(&self, notification: OutgoingNotification) { + let outgoing_message = OutgoingMessage::Notification(notification); + let _ = self.sender.send(outgoing_message); + } + + pub(crate) async fn send_error(&self, id: RequestId, error: JSONRPCErrorError) { + let outgoing_message = OutgoingMessage::Error(OutgoingError { id, error }); + let _ = self.sender.send(outgoing_message); + } +} + +/// Outgoing message from the server to the client. +pub(crate) enum OutgoingMessage { + Request(OutgoingRequest), + Notification(OutgoingNotification), + /// AppServerNotification is specific to the case where this is run as an + /// "app server" as opposed to an MCP server. + AppServerNotification(ServerNotification), + Response(OutgoingResponse), + Error(OutgoingError), +} + +impl From for JSONRPCMessage { + fn from(val: OutgoingMessage) -> Self { + use OutgoingMessage::*; + match val { + Request(OutgoingRequest { id, method, params }) => { + JSONRPCMessage::Request(JSONRPCRequest { + jsonrpc: JSONRPC_VERSION.into(), + id, + method, + params, + }) + } + Notification(OutgoingNotification { method, params }) => { + JSONRPCMessage::Notification(JSONRPCNotification { + jsonrpc: JSONRPC_VERSION.into(), + method, + params, + }) + } + AppServerNotification(notification) => { + let method = notification.to_string(); + let params = match notification.to_params() { + Ok(params) => Some(params), + Err(err) => { + warn!("failed to serialize notification params: {err}"); + None + } + }; + JSONRPCMessage::Notification(JSONRPCNotification { + jsonrpc: JSONRPC_VERSION.into(), + method, + params, + }) + } + Response(OutgoingResponse { id, result }) => { + JSONRPCMessage::Response(JSONRPCResponse { + jsonrpc: JSONRPC_VERSION.into(), + id, + result, + }) + } + Error(OutgoingError { id, error }) => JSONRPCMessage::Error(JSONRPCError { + jsonrpc: JSONRPC_VERSION.into(), + id, + error, + }), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub(crate) struct OutgoingRequest { + pub id: RequestId, + pub method: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub params: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub(crate) struct OutgoingNotification { + pub method: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub params: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub(crate) struct OutgoingResponse { + pub id: RequestId, + pub result: Result, +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub(crate) struct OutgoingError { + pub error: JSONRPCErrorError, + pub id: RequestId, +} + +#[cfg(test)] +mod tests { + use codex_protocol::mcp_protocol::LoginChatGptCompleteNotification; + use pretty_assertions::assert_eq; + use serde_json::json; + use uuid::Uuid; + + use super::*; + + #[test] + fn verify_server_notification_serialization() { + let notification = + ServerNotification::LoginChatGptComplete(LoginChatGptCompleteNotification { + login_id: Uuid::nil(), + success: true, + error: None, + }); + + let jsonrpc_notification: JSONRPCMessage = + OutgoingMessage::AppServerNotification(notification).into(); + assert_eq!( + JSONRPCMessage::Notification(JSONRPCNotification { + jsonrpc: "2.0".into(), + method: "loginChatGptComplete".into(), + params: Some(json!({ + "loginId": Uuid::nil(), + "success": true, + })), + }), + jsonrpc_notification, + "ensure the strum macros serialize the method field correctly" + ); + } +} diff --git a/codex-rs/app-server/tests/all.rs b/codex-rs/app-server/tests/all.rs new file mode 100644 index 0000000000..7e136e4cce --- /dev/null +++ b/codex-rs/app-server/tests/all.rs @@ -0,0 +1,3 @@ +// Single integration test binary that aggregates all test modules. +// The submodules live in `tests/suite/`. +mod suite; diff --git a/codex-rs/app-server/tests/common/Cargo.toml b/codex-rs/app-server/tests/common/Cargo.toml new file mode 100644 index 0000000000..4cc711fb6b --- /dev/null +++ b/codex-rs/app-server/tests/common/Cargo.toml @@ -0,0 +1,22 @@ +[package] +edition = "2024" +name = "app_test_support" +version = { workspace = true } + +[lib] +path = "lib.rs" + +[dependencies] +anyhow = { workspace = true } +assert_cmd = { workspace = true } +codex-protocol = { workspace = true } +mcp-types = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = [ + "io-std", + "macros", + "process", + "rt-multi-thread", +] } +wiremock = { workspace = true } diff --git a/codex-rs/app-server/tests/common/lib.rs b/codex-rs/app-server/tests/common/lib.rs new file mode 100644 index 0000000000..d088b184ea --- /dev/null +++ b/codex-rs/app-server/tests/common/lib.rs @@ -0,0 +1,17 @@ +mod mcp_process; +mod mock_model_server; +mod responses; + +pub use mcp_process::McpProcess; +use mcp_types::JSONRPCResponse; +pub use mock_model_server::create_mock_chat_completions_server; +pub use responses::create_apply_patch_sse_response; +pub use responses::create_final_assistant_message_sse_response; +pub use responses::create_shell_sse_response; +use serde::de::DeserializeOwned; + +pub fn to_response(response: JSONRPCResponse) -> anyhow::Result { + let value = serde_json::to_value(response.result)?; + let codex_response = serde_json::from_value(value)?; + Ok(codex_response) +} diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs new file mode 100644 index 0000000000..cbf54cecf0 --- /dev/null +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -0,0 +1,477 @@ +use std::path::Path; +use std::process::Stdio; +use std::sync::atomic::AtomicI64; +use std::sync::atomic::Ordering; +use tokio::io::AsyncBufReadExt; +use tokio::io::AsyncWriteExt; +use tokio::io::BufReader; +use tokio::process::Child; +use tokio::process::ChildStdin; +use tokio::process::ChildStdout; + +use anyhow::Context; +use assert_cmd::prelude::*; +use codex_protocol::mcp_protocol::AddConversationListenerParams; +use codex_protocol::mcp_protocol::ArchiveConversationParams; +use codex_protocol::mcp_protocol::CancelLoginChatGptParams; +use codex_protocol::mcp_protocol::ClientInfo; +use codex_protocol::mcp_protocol::ClientNotification; +use codex_protocol::mcp_protocol::GetAuthStatusParams; +use codex_protocol::mcp_protocol::InitializeParams; +use codex_protocol::mcp_protocol::InterruptConversationParams; +use codex_protocol::mcp_protocol::ListConversationsParams; +use codex_protocol::mcp_protocol::LoginApiKeyParams; +use codex_protocol::mcp_protocol::NewConversationParams; +use codex_protocol::mcp_protocol::RemoveConversationListenerParams; +use codex_protocol::mcp_protocol::ResumeConversationParams; +use codex_protocol::mcp_protocol::SendUserMessageParams; +use codex_protocol::mcp_protocol::SendUserTurnParams; +use codex_protocol::mcp_protocol::SetDefaultModelParams; + +use mcp_types::JSONRPC_VERSION; +use mcp_types::JSONRPCMessage; +use mcp_types::JSONRPCNotification; +use mcp_types::JSONRPCRequest; +use mcp_types::JSONRPCResponse; +use mcp_types::RequestId; +use std::process::Command as StdCommand; +use tokio::process::Command; + +pub struct McpProcess { + next_request_id: AtomicI64, + /// Retain this child process until the client is dropped. The Tokio runtime + /// will make a "best effort" to reap the process after it exits, but it is + /// not a guarantee. See the `kill_on_drop` documentation for details. + #[allow(dead_code)] + process: Child, + stdin: ChildStdin, + stdout: BufReader, +} + +impl McpProcess { + pub async fn new(codex_home: &Path) -> anyhow::Result { + Self::new_with_env(codex_home, &[]).await + } + + /// Creates a new MCP process, allowing tests to override or remove + /// specific environment variables for the child process only. + /// + /// Pass a tuple of (key, Some(value)) to set/override, or (key, None) to + /// remove a variable from the child's environment. + pub async fn new_with_env( + codex_home: &Path, + env_overrides: &[(&str, Option<&str>)], + ) -> anyhow::Result { + // Use assert_cmd to locate the binary path and then switch to tokio::process::Command + let std_cmd = StdCommand::cargo_bin("codex-app-server") + .context("should find binary for codex-mcp-server")?; + + let program = std_cmd.get_program().to_owned(); + + let mut cmd = Command::new(program); + + cmd.stdin(Stdio::piped()); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + cmd.env("CODEX_HOME", codex_home); + cmd.env("RUST_LOG", "debug"); + + for (k, v) in env_overrides { + match v { + Some(val) => { + cmd.env(k, val); + } + None => { + cmd.env_remove(k); + } + } + } + + let mut process = cmd + .kill_on_drop(true) + .spawn() + .context("codex-mcp-server proc should start")?; + let stdin = process + .stdin + .take() + .ok_or_else(|| anyhow::format_err!("mcp should have stdin fd"))?; + let stdout = process + .stdout + .take() + .ok_or_else(|| anyhow::format_err!("mcp should have stdout fd"))?; + let stdout = BufReader::new(stdout); + + // Forward child's stderr to our stderr so failures are visible even + // when stdout/stderr are captured by the test harness. + if let Some(stderr) = process.stderr.take() { + let mut stderr_reader = BufReader::new(stderr).lines(); + tokio::spawn(async move { + while let Ok(Some(line)) = stderr_reader.next_line().await { + eprintln!("[mcp stderr] {line}"); + } + }); + } + Ok(Self { + next_request_id: AtomicI64::new(0), + process, + stdin, + stdout, + }) + } + + /// Performs the initialization handshake with the MCP server. + pub async fn initialize(&mut self) -> anyhow::Result<()> { + let params = Some(serde_json::to_value(InitializeParams { + client_info: ClientInfo { + name: "codex-app-server-tests".to_string(), + title: None, + version: "0.1.0".to_string(), + }, + })?); + let req_id = self.send_request("initialize", params).await?; + let initialized = self.read_jsonrpc_message().await?; + let JSONRPCMessage::Response(response) = initialized else { + unreachable!("expected JSONRPCMessage::Response for initialize, got {initialized:?}"); + }; + if response.id != RequestId::Integer(req_id) { + anyhow::bail!( + "initialize response id mismatch: expected {}, got {:?}", + req_id, + response.id + ); + } + + // Send notifications/initialized to ack the response. + self.send_notification(ClientNotification::Initialized) + .await?; + + Ok(()) + } + + /// Send a `newConversation` JSON-RPC request. + pub async fn send_new_conversation_request( + &mut self, + params: NewConversationParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("newConversation", params).await + } + + /// Send an `archiveConversation` JSON-RPC request. + pub async fn send_archive_conversation_request( + &mut self, + params: ArchiveConversationParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("archiveConversation", params).await + } + + /// Send an `addConversationListener` JSON-RPC request. + pub async fn send_add_conversation_listener_request( + &mut self, + params: AddConversationListenerParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("addConversationListener", params).await + } + + /// Send a `sendUserMessage` JSON-RPC request with a single text item. + pub async fn send_send_user_message_request( + &mut self, + params: SendUserMessageParams, + ) -> anyhow::Result { + // Wire format expects variants in camelCase; text item uses external tagging. + let params = Some(serde_json::to_value(params)?); + self.send_request("sendUserMessage", params).await + } + + /// Send a `removeConversationListener` JSON-RPC request. + pub async fn send_remove_conversation_listener_request( + &mut self, + params: RemoveConversationListenerParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("removeConversationListener", params) + .await + } + + /// Send a `sendUserTurn` JSON-RPC request. + pub async fn send_send_user_turn_request( + &mut self, + params: SendUserTurnParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("sendUserTurn", params).await + } + + /// Send a `interruptConversation` JSON-RPC request. + pub async fn send_interrupt_conversation_request( + &mut self, + params: InterruptConversationParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("interruptConversation", params).await + } + + /// Send a `getAuthStatus` JSON-RPC request. + pub async fn send_get_auth_status_request( + &mut self, + params: GetAuthStatusParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("getAuthStatus", params).await + } + + /// Send a `getUserSavedConfig` JSON-RPC request. + pub async fn send_get_user_saved_config_request(&mut self) -> anyhow::Result { + self.send_request("getUserSavedConfig", None).await + } + + /// Send a `getUserAgent` JSON-RPC request. + pub async fn send_get_user_agent_request(&mut self) -> anyhow::Result { + self.send_request("getUserAgent", None).await + } + + /// Send a `userInfo` JSON-RPC request. + pub async fn send_user_info_request(&mut self) -> anyhow::Result { + self.send_request("userInfo", None).await + } + + /// Send a `setDefaultModel` JSON-RPC request. + pub async fn send_set_default_model_request( + &mut self, + params: SetDefaultModelParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("setDefaultModel", params).await + } + + /// Send a `listConversations` JSON-RPC request. + pub async fn send_list_conversations_request( + &mut self, + params: ListConversationsParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("listConversations", params).await + } + + /// Send a `resumeConversation` JSON-RPC request. + pub async fn send_resume_conversation_request( + &mut self, + params: ResumeConversationParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("resumeConversation", params).await + } + + /// Send a `loginApiKey` JSON-RPC request. + pub async fn send_login_api_key_request( + &mut self, + params: LoginApiKeyParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("loginApiKey", params).await + } + + /// Send a `loginChatGpt` JSON-RPC request. + pub async fn send_login_chat_gpt_request(&mut self) -> anyhow::Result { + self.send_request("loginChatGpt", None).await + } + + /// Send a `cancelLoginChatGpt` JSON-RPC request. + pub async fn send_cancel_login_chat_gpt_request( + &mut self, + params: CancelLoginChatGptParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("cancelLoginChatGpt", params).await + } + + /// Send a `logoutChatGpt` JSON-RPC request. + pub async fn send_logout_chat_gpt_request(&mut self) -> anyhow::Result { + self.send_request("logoutChatGpt", None).await + } + + /// Send a `fuzzyFileSearch` JSON-RPC request. + pub async fn send_fuzzy_file_search_request( + &mut self, + query: &str, + roots: Vec, + cancellation_token: Option, + ) -> anyhow::Result { + let mut params = serde_json::json!({ + "query": query, + "roots": roots, + }); + if let Some(token) = cancellation_token { + params["cancellationToken"] = serde_json::json!(token); + } + self.send_request("fuzzyFileSearch", Some(params)).await + } + + async fn send_request( + &mut self, + method: &str, + params: Option, + ) -> anyhow::Result { + let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed); + + let message = JSONRPCMessage::Request(JSONRPCRequest { + jsonrpc: JSONRPC_VERSION.into(), + id: RequestId::Integer(request_id), + method: method.to_string(), + params, + }); + self.send_jsonrpc_message(message).await?; + Ok(request_id) + } + + pub async fn send_response( + &mut self, + id: RequestId, + result: serde_json::Value, + ) -> anyhow::Result<()> { + self.send_jsonrpc_message(JSONRPCMessage::Response(JSONRPCResponse { + jsonrpc: JSONRPC_VERSION.into(), + id, + result, + })) + .await + } + + pub async fn send_notification( + &mut self, + notification: ClientNotification, + ) -> anyhow::Result<()> { + let value = serde_json::to_value(notification)?; + self.send_jsonrpc_message(JSONRPCMessage::Notification(JSONRPCNotification { + jsonrpc: JSONRPC_VERSION.into(), + method: value + .get("method") + .and_then(|m| m.as_str()) + .ok_or_else(|| anyhow::format_err!("notification missing method field"))? + .to_string(), + params: value.get("params").cloned(), + })) + .await + } + + async fn send_jsonrpc_message(&mut self, message: JSONRPCMessage) -> anyhow::Result<()> { + eprintln!("writing message to stdin: {message:?}"); + let payload = serde_json::to_string(&message)?; + self.stdin.write_all(payload.as_bytes()).await?; + self.stdin.write_all(b"\n").await?; + self.stdin.flush().await?; + Ok(()) + } + + async fn read_jsonrpc_message(&mut self) -> anyhow::Result { + let mut line = String::new(); + self.stdout.read_line(&mut line).await?; + let message = serde_json::from_str::(&line)?; + eprintln!("read message from stdout: {message:?}"); + Ok(message) + } + + pub async fn read_stream_until_request_message(&mut self) -> anyhow::Result { + eprintln!("in read_stream_until_request_message()"); + + loop { + let message = self.read_jsonrpc_message().await?; + + match message { + JSONRPCMessage::Notification(_) => { + eprintln!("notification: {message:?}"); + } + JSONRPCMessage::Request(jsonrpc_request) => { + return Ok(jsonrpc_request); + } + JSONRPCMessage::Error(_) => { + anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}"); + } + JSONRPCMessage::Response(_) => { + anyhow::bail!("unexpected JSONRPCMessage::Response: {message:?}"); + } + } + } + } + + pub async fn read_stream_until_response_message( + &mut self, + request_id: RequestId, + ) -> anyhow::Result { + eprintln!("in read_stream_until_response_message({request_id:?})"); + + loop { + let message = self.read_jsonrpc_message().await?; + match message { + JSONRPCMessage::Notification(_) => { + eprintln!("notification: {message:?}"); + } + JSONRPCMessage::Request(_) => { + anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}"); + } + JSONRPCMessage::Error(_) => { + anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}"); + } + JSONRPCMessage::Response(jsonrpc_response) => { + if jsonrpc_response.id == request_id { + return Ok(jsonrpc_response); + } + } + } + } + } + + pub async fn read_stream_until_error_message( + &mut self, + request_id: RequestId, + ) -> anyhow::Result { + loop { + let message = self.read_jsonrpc_message().await?; + match message { + JSONRPCMessage::Notification(_) => { + eprintln!("notification: {message:?}"); + } + JSONRPCMessage::Request(_) => { + anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}"); + } + JSONRPCMessage::Response(_) => { + // Keep scanning; we're waiting for an error with matching id. + } + JSONRPCMessage::Error(err) => { + if err.id == request_id { + return Ok(err); + } + } + } + } + } + + pub async fn read_stream_until_notification_message( + &mut self, + method: &str, + ) -> anyhow::Result { + eprintln!("in read_stream_until_notification_message({method})"); + + loop { + let message = self.read_jsonrpc_message().await?; + match message { + JSONRPCMessage::Notification(notification) => { + if notification.method == method { + return Ok(notification); + } + } + JSONRPCMessage::Request(_) => { + anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}"); + } + JSONRPCMessage::Error(_) => { + anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}"); + } + JSONRPCMessage::Response(_) => { + anyhow::bail!("unexpected JSONRPCMessage::Response: {message:?}"); + } + } + } + } +} diff --git a/codex-rs/app-server/tests/common/mock_model_server.rs b/codex-rs/app-server/tests/common/mock_model_server.rs new file mode 100644 index 0000000000..be7f3eb5b3 --- /dev/null +++ b/codex-rs/app-server/tests/common/mock_model_server.rs @@ -0,0 +1,47 @@ +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; + +use wiremock::Mock; +use wiremock::MockServer; +use wiremock::Respond; +use wiremock::ResponseTemplate; +use wiremock::matchers::method; +use wiremock::matchers::path; + +/// Create a mock server that will provide the responses, in order, for +/// requests to the `/v1/chat/completions` endpoint. +pub async fn create_mock_chat_completions_server(responses: Vec) -> MockServer { + let server = MockServer::start().await; + + let num_calls = responses.len(); + let seq_responder = SeqResponder { + num_calls: AtomicUsize::new(0), + responses, + }; + + Mock::given(method("POST")) + .and(path("/v1/chat/completions")) + .respond_with(seq_responder) + .expect(num_calls as u64) + .mount(&server) + .await; + + server +} + +struct SeqResponder { + num_calls: AtomicUsize, + responses: Vec, +} + +impl Respond for SeqResponder { + fn respond(&self, _: &wiremock::Request) -> ResponseTemplate { + let call_num = self.num_calls.fetch_add(1, Ordering::SeqCst); + match self.responses.get(call_num) { + Some(response) => ResponseTemplate::new(200) + .insert_header("content-type", "text/event-stream") + .set_body_raw(response.clone(), "text/event-stream"), + None => panic!("no response for {call_num}"), + } + } +} diff --git a/codex-rs/app-server/tests/common/responses.rs b/codex-rs/app-server/tests/common/responses.rs new file mode 100644 index 0000000000..9a827fb986 --- /dev/null +++ b/codex-rs/app-server/tests/common/responses.rs @@ -0,0 +1,95 @@ +use serde_json::json; +use std::path::Path; + +pub fn create_shell_sse_response( + command: Vec, + workdir: Option<&Path>, + timeout_ms: Option, + call_id: &str, +) -> anyhow::Result { + // The `arguments`` for the `shell` tool is a serialized JSON object. + let tool_call_arguments = serde_json::to_string(&json!({ + "command": command, + "workdir": workdir.map(|w| w.to_string_lossy()), + "timeout": timeout_ms + }))?; + let tool_call = json!({ + "choices": [ + { + "delta": { + "tool_calls": [ + { + "id": call_id, + "function": { + "name": "shell", + "arguments": tool_call_arguments + } + } + ] + }, + "finish_reason": "tool_calls" + } + ] + }); + + let sse = format!( + "data: {}\n\ndata: DONE\n\n", + serde_json::to_string(&tool_call)? + ); + Ok(sse) +} + +pub fn create_final_assistant_message_sse_response(message: &str) -> anyhow::Result { + let assistant_message = json!({ + "choices": [ + { + "delta": { + "content": message + }, + "finish_reason": "stop" + } + ] + }); + + let sse = format!( + "data: {}\n\ndata: DONE\n\n", + serde_json::to_string(&assistant_message)? + ); + Ok(sse) +} + +pub fn create_apply_patch_sse_response( + patch_content: &str, + call_id: &str, +) -> anyhow::Result { + // Use shell command to call apply_patch with heredoc format + let shell_command = format!("apply_patch <<'EOF'\n{patch_content}\nEOF"); + let tool_call_arguments = serde_json::to_string(&json!({ + "command": ["bash", "-lc", shell_command] + }))?; + + let tool_call = json!({ + "choices": [ + { + "delta": { + "tool_calls": [ + { + "id": call_id, + "function": { + "name": "shell", + "arguments": tool_call_arguments + } + } + ] + }, + "finish_reason": "tool_calls" + } + ] + }); + + let sse = format!( + "data: {}\n\ndata: DONE\n\n", + serde_json::to_string(&tool_call)? + ); + Ok(sse) +} diff --git a/codex-rs/mcp-server/tests/suite/archive_conversation.rs b/codex-rs/app-server/tests/suite/archive_conversation.rs similarity index 98% rename from codex-rs/mcp-server/tests/suite/archive_conversation.rs rename to codex-rs/app-server/tests/suite/archive_conversation.rs index e54a99896c..65a1589763 100644 --- a/codex-rs/mcp-server/tests/suite/archive_conversation.rs +++ b/codex-rs/app-server/tests/suite/archive_conversation.rs @@ -1,12 +1,12 @@ use std::path::Path; +use app_test_support::McpProcess; +use app_test_support::to_response; use codex_core::ARCHIVED_SESSIONS_SUBDIR; use codex_protocol::mcp_protocol::ArchiveConversationParams; use codex_protocol::mcp_protocol::ArchiveConversationResponse; use codex_protocol::mcp_protocol::NewConversationParams; use codex_protocol::mcp_protocol::NewConversationResponse; -use mcp_test_support::McpProcess; -use mcp_test_support::to_response; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; use tempfile::TempDir; diff --git a/codex-rs/mcp-server/tests/suite/auth.rs b/codex-rs/app-server/tests/suite/auth.rs similarity index 99% rename from codex-rs/mcp-server/tests/suite/auth.rs rename to codex-rs/app-server/tests/suite/auth.rs index 6681fd7546..b19203d880 100644 --- a/codex-rs/mcp-server/tests/suite/auth.rs +++ b/codex-rs/app-server/tests/suite/auth.rs @@ -1,12 +1,12 @@ use std::path::Path; +use app_test_support::McpProcess; +use app_test_support::to_response; use codex_protocol::mcp_protocol::AuthMode; use codex_protocol::mcp_protocol::GetAuthStatusParams; use codex_protocol::mcp_protocol::GetAuthStatusResponse; use codex_protocol::mcp_protocol::LoginApiKeyParams; use codex_protocol::mcp_protocol::LoginApiKeyResponse; -use mcp_test_support::McpProcess; -use mcp_test_support::to_response; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; use pretty_assertions::assert_eq; diff --git a/codex-rs/mcp-server/tests/suite/codex_message_processor_flow.rs b/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs similarity index 98% rename from codex-rs/mcp-server/tests/suite/codex_message_processor_flow.rs rename to codex-rs/app-server/tests/suite/codex_message_processor_flow.rs index 50a480b4a0..7527411223 100644 --- a/codex-rs/mcp-server/tests/suite/codex_message_processor_flow.rs +++ b/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs @@ -1,5 +1,10 @@ use std::path::Path; +use app_test_support::McpProcess; +use app_test_support::create_final_assistant_message_sse_response; +use app_test_support::create_mock_chat_completions_server; +use app_test_support::create_shell_sse_response; +use app_test_support::to_response; use codex_core::protocol::AskForApproval; use codex_core::protocol::SandboxPolicy; use codex_core::protocol_config_types::ReasoningEffort; @@ -16,11 +21,6 @@ use codex_protocol::mcp_protocol::SendUserMessageParams; use codex_protocol::mcp_protocol::SendUserMessageResponse; use codex_protocol::mcp_protocol::SendUserTurnParams; use codex_protocol::mcp_protocol::SendUserTurnResponse; -use mcp_test_support::McpProcess; -use mcp_test_support::create_final_assistant_message_sse_response; -use mcp_test_support::create_mock_chat_completions_server; -use mcp_test_support::create_shell_sse_response; -use mcp_test_support::to_response; use mcp_types::JSONRPCNotification; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; diff --git a/codex-rs/mcp-server/tests/suite/config.rs b/codex-rs/app-server/tests/suite/config.rs similarity index 98% rename from codex-rs/mcp-server/tests/suite/config.rs rename to codex-rs/app-server/tests/suite/config.rs index da64648c49..13cb6c4a2f 100644 --- a/codex-rs/mcp-server/tests/suite/config.rs +++ b/codex-rs/app-server/tests/suite/config.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; use std::path::Path; +use app_test_support::McpProcess; +use app_test_support::to_response; use codex_core::protocol::AskForApproval; use codex_protocol::config_types::ReasoningEffort; use codex_protocol::config_types::ReasoningSummary; @@ -11,8 +13,6 @@ use codex_protocol::mcp_protocol::Profile; use codex_protocol::mcp_protocol::SandboxSettings; use codex_protocol::mcp_protocol::Tools; use codex_protocol::mcp_protocol::UserSavedConfig; -use mcp_test_support::McpProcess; -use mcp_test_support::to_response; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; use pretty_assertions::assert_eq; diff --git a/codex-rs/mcp-server/tests/suite/create_conversation.rs b/codex-rs/app-server/tests/suite/create_conversation.rs similarity index 96% rename from codex-rs/mcp-server/tests/suite/create_conversation.rs rename to codex-rs/app-server/tests/suite/create_conversation.rs index 1b62d01d46..d5896fffcb 100644 --- a/codex-rs/mcp-server/tests/suite/create_conversation.rs +++ b/codex-rs/app-server/tests/suite/create_conversation.rs @@ -1,5 +1,9 @@ use std::path::Path; +use app_test_support::McpProcess; +use app_test_support::create_final_assistant_message_sse_response; +use app_test_support::create_mock_chat_completions_server; +use app_test_support::to_response; use codex_protocol::mcp_protocol::AddConversationListenerParams; use codex_protocol::mcp_protocol::AddConversationSubscriptionResponse; use codex_protocol::mcp_protocol::InputItem; @@ -7,10 +11,6 @@ use codex_protocol::mcp_protocol::NewConversationParams; use codex_protocol::mcp_protocol::NewConversationResponse; use codex_protocol::mcp_protocol::SendUserMessageParams; use codex_protocol::mcp_protocol::SendUserMessageResponse; -use mcp_test_support::McpProcess; -use mcp_test_support::create_final_assistant_message_sse_response; -use mcp_test_support::create_mock_chat_completions_server; -use mcp_test_support::to_response; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; use pretty_assertions::assert_eq; diff --git a/codex-rs/mcp-server/tests/suite/fuzzy_file_search.rs b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs similarity index 99% rename from codex-rs/mcp-server/tests/suite/fuzzy_file_search.rs rename to codex-rs/app-server/tests/suite/fuzzy_file_search.rs index e4aa0add9c..12cbb7d574 100644 --- a/codex-rs/mcp-server/tests/suite/fuzzy_file_search.rs +++ b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs @@ -1,4 +1,4 @@ -use mcp_test_support::McpProcess; +use app_test_support::McpProcess; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; use pretty_assertions::assert_eq; diff --git a/codex-rs/mcp-server/tests/suite/interrupt.rs b/codex-rs/app-server/tests/suite/interrupt.rs similarity index 95% rename from codex-rs/mcp-server/tests/suite/interrupt.rs rename to codex-rs/app-server/tests/suite/interrupt.rs index 232b695d1d..087f1b6078 100644 --- a/codex-rs/mcp-server/tests/suite/interrupt.rs +++ b/codex-rs/app-server/tests/suite/interrupt.rs @@ -1,5 +1,5 @@ #![cfg(unix)] -// Support code lives in the `mcp_test_support` crate under tests/common. +// Support code lives in the `app_test_support` crate under tests/common. use std::path::Path; @@ -17,10 +17,10 @@ use mcp_types::RequestId; use tempfile::TempDir; use tokio::time::timeout; -use mcp_test_support::McpProcess; -use mcp_test_support::create_mock_chat_completions_server; -use mcp_test_support::create_shell_sse_response; -use mcp_test_support::to_response; +use app_test_support::McpProcess; +use app_test_support::create_mock_chat_completions_server; +use app_test_support::create_shell_sse_response; +use app_test_support::to_response; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); diff --git a/codex-rs/mcp-server/tests/suite/list_resume.rs b/codex-rs/app-server/tests/suite/list_resume.rs similarity index 87% rename from codex-rs/mcp-server/tests/suite/list_resume.rs rename to codex-rs/app-server/tests/suite/list_resume.rs index 9302b42990..9e91fdd9cd 100644 --- a/codex-rs/mcp-server/tests/suite/list_resume.rs +++ b/codex-rs/app-server/tests/suite/list_resume.rs @@ -1,13 +1,15 @@ use std::fs; use std::path::Path; +use app_test_support::McpProcess; +use app_test_support::to_response; use codex_protocol::mcp_protocol::ListConversationsParams; use codex_protocol::mcp_protocol::ListConversationsResponse; use codex_protocol::mcp_protocol::NewConversationParams; // reused for overrides shape use codex_protocol::mcp_protocol::ResumeConversationParams; use codex_protocol::mcp_protocol::ResumeConversationResponse; -use mcp_test_support::McpProcess; -use mcp_test_support::to_response; +use codex_protocol::mcp_protocol::ServerNotification; +use codex_protocol::mcp_protocol::SessionConfiguredNotification; use mcp_types::JSONRPCNotification; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; @@ -111,23 +113,28 @@ async fn test_list_and_resume_conversations() { .await .expect("send resumeConversation"); - // Expect a codex/event notification with msg.type == session_configured + // Expect a codex/event notification with msg.type == sessionConfigured let notification: JSONRPCNotification = timeout( DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("codex/event"), + mcp.read_stream_until_notification_message("sessionConfigured"), ) .await - .expect("session_configured notification timeout") - .expect("session_configured notification"); - // Basic shape assertion: ensure event type is session_configured - let msg_type = notification - .params - .as_ref() - .and_then(|p| p.get("msg")) - .and_then(|m| m.get("type")) - .and_then(|t| t.as_str()) - .unwrap_or(""); - assert_eq!(msg_type, "session_configured"); + .expect("sessionConfigured notification timeout") + .expect("sessionConfigured notification"); + let session_configured: ServerNotification = notification + .try_into() + .expect("deserialize sessionConfigured notification"); + // Basic shape assertion: ensure event type is sessionConfigured + let ServerNotification::SessionConfigured(SessionConfiguredNotification { + model, + rollout_path, + .. + }) = session_configured + else { + unreachable!("expected sessionConfigured notification"); + }; + assert_eq!(model, "o3"); + assert_eq!(items[0].path.clone(), rollout_path); // Then the response for resumeConversation let resume_resp: JSONRPCResponse = timeout( diff --git a/codex-rs/mcp-server/tests/suite/login.rs b/codex-rs/app-server/tests/suite/login.rs similarity index 98% rename from codex-rs/mcp-server/tests/suite/login.rs rename to codex-rs/app-server/tests/suite/login.rs index 071154c643..475af9ab57 100644 --- a/codex-rs/mcp-server/tests/suite/login.rs +++ b/codex-rs/app-server/tests/suite/login.rs @@ -1,6 +1,8 @@ use std::path::Path; use std::time::Duration; +use app_test_support::McpProcess; +use app_test_support::to_response; use codex_login::login_with_api_key; use codex_protocol::mcp_protocol::CancelLoginChatGptParams; use codex_protocol::mcp_protocol::CancelLoginChatGptResponse; @@ -8,8 +10,6 @@ use codex_protocol::mcp_protocol::GetAuthStatusParams; use codex_protocol::mcp_protocol::GetAuthStatusResponse; use codex_protocol::mcp_protocol::LoginChatGptResponse; use codex_protocol::mcp_protocol::LogoutChatGptResponse; -use mcp_test_support::McpProcess; -use mcp_test_support::to_response; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; use tempfile::TempDir; diff --git a/codex-rs/app-server/tests/suite/mod.rs b/codex-rs/app-server/tests/suite/mod.rs new file mode 100644 index 0000000000..78ce310e74 --- /dev/null +++ b/codex-rs/app-server/tests/suite/mod.rs @@ -0,0 +1,13 @@ +mod archive_conversation; +mod auth; +mod codex_message_processor_flow; +mod config; +mod create_conversation; +mod fuzzy_file_search; +mod interrupt; +mod list_resume; +mod login; +mod send_message; +mod set_default_model; +mod user_agent; +mod user_info; diff --git a/codex-rs/mcp-server/tests/suite/send_message.rs b/codex-rs/app-server/tests/suite/send_message.rs similarity index 97% rename from codex-rs/mcp-server/tests/suite/send_message.rs rename to codex-rs/app-server/tests/suite/send_message.rs index 158cb12d1c..81da0096f4 100644 --- a/codex-rs/mcp-server/tests/suite/send_message.rs +++ b/codex-rs/app-server/tests/suite/send_message.rs @@ -1,5 +1,9 @@ use std::path::Path; +use app_test_support::McpProcess; +use app_test_support::create_final_assistant_message_sse_response; +use app_test_support::create_mock_chat_completions_server; +use app_test_support::to_response; use codex_protocol::mcp_protocol::AddConversationListenerParams; use codex_protocol::mcp_protocol::AddConversationSubscriptionResponse; use codex_protocol::mcp_protocol::ConversationId; @@ -8,10 +12,6 @@ use codex_protocol::mcp_protocol::NewConversationParams; use codex_protocol::mcp_protocol::NewConversationResponse; use codex_protocol::mcp_protocol::SendUserMessageParams; use codex_protocol::mcp_protocol::SendUserMessageResponse; -use mcp_test_support::McpProcess; -use mcp_test_support::create_final_assistant_message_sse_response; -use mcp_test_support::create_mock_chat_completions_server; -use mcp_test_support::to_response; use mcp_types::JSONRPCNotification; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; diff --git a/codex-rs/mcp-server/tests/suite/set_default_model.rs b/codex-rs/app-server/tests/suite/set_default_model.rs similarity index 97% rename from codex-rs/mcp-server/tests/suite/set_default_model.rs rename to codex-rs/app-server/tests/suite/set_default_model.rs index f7e1041fa7..ce0d79d497 100644 --- a/codex-rs/mcp-server/tests/suite/set_default_model.rs +++ b/codex-rs/app-server/tests/suite/set_default_model.rs @@ -1,10 +1,10 @@ use std::path::Path; +use app_test_support::McpProcess; +use app_test_support::to_response; use codex_core::config::ConfigToml; use codex_protocol::mcp_protocol::SetDefaultModelParams; use codex_protocol::mcp_protocol::SetDefaultModelResponse; -use mcp_test_support::McpProcess; -use mcp_test_support::to_response; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; use pretty_assertions::assert_eq; diff --git a/codex-rs/mcp-server/tests/suite/user_agent.rs b/codex-rs/app-server/tests/suite/user_agent.rs similarity index 91% rename from codex-rs/mcp-server/tests/suite/user_agent.rs rename to codex-rs/app-server/tests/suite/user_agent.rs index 718e145250..a7f75eab37 100644 --- a/codex-rs/mcp-server/tests/suite/user_agent.rs +++ b/codex-rs/app-server/tests/suite/user_agent.rs @@ -1,6 +1,6 @@ +use app_test_support::McpProcess; +use app_test_support::to_response; use codex_protocol::mcp_protocol::GetUserAgentResponse; -use mcp_test_support::McpProcess; -use mcp_test_support::to_response; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; use pretty_assertions::assert_eq; @@ -35,7 +35,7 @@ async fn get_user_agent_returns_current_codex_user_agent() { let os_info = os_info::get(); let user_agent = format!( - "codex_cli_rs/0.0.0 ({} {}; {}) {} (elicitation test; 0.0.0)", + "codex_cli_rs/0.0.0 ({} {}; {}) {} (codex-app-server-tests; 0.1.0)", os_info.os_type(), os_info.version(), os_info.architecture().unwrap_or("unknown"), diff --git a/codex-rs/mcp-server/tests/suite/user_info.rs b/codex-rs/app-server/tests/suite/user_info.rs similarity index 97% rename from codex-rs/mcp-server/tests/suite/user_info.rs rename to codex-rs/app-server/tests/suite/user_info.rs index 7bcb2acc6b..10ca7d330a 100644 --- a/codex-rs/mcp-server/tests/suite/user_info.rs +++ b/codex-rs/app-server/tests/suite/user_info.rs @@ -1,6 +1,8 @@ use std::time::Duration; use anyhow::Context; +use app_test_support::McpProcess; +use app_test_support::to_response; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use codex_core::auth::AuthDotJson; @@ -9,8 +11,6 @@ use codex_core::auth::write_auth_json; use codex_core::token_data::IdTokenInfo; use codex_core::token_data::TokenData; use codex_protocol::mcp_protocol::UserInfoResponse; -use mcp_test_support::McpProcess; -use mcp_test_support::to_response; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; use pretty_assertions::assert_eq; diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 618f4b7b03..c28b90a2b1 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -18,6 +18,7 @@ workspace = true anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } clap_complete = { workspace = true } +codex-app-server = { workspace = true } codex-arg0 = { workspace = true } codex-chatgpt = { workspace = true } codex-common = { workspace = true, features = ["cli"] } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index df0d30aba9..d0d777f568 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -66,6 +66,12 @@ enum Subcommand { /// [experimental] Run Codex as an MCP server and manage MCP servers. Mcp(McpCli), + /// [experimental] Run the Codex MCP server (stdio transport). + McpServer, + + /// [experimental] Run the app server. + AppServer, + /// Run the Protocol stream via stdin/stdout #[clap(visible_alias = "p")] Proto(ProtoCli), @@ -260,10 +266,16 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() ); codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?; } + Some(Subcommand::McpServer) => { + codex_mcp_server::run_main(codex_linux_sandbox_exe, root_config_overrides).await?; + } Some(Subcommand::Mcp(mut mcp_cli)) => { // Propagate any root-level config overrides (e.g. `-c key=value`). prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone()); - mcp_cli.run(codex_linux_sandbox_exe).await?; + mcp_cli.run().await?; + } + Some(Subcommand::AppServer) => { + codex_app_server::run_main(codex_linux_sandbox_exe, root_config_overrides).await?; } Some(Subcommand::Resume(ResumeCommand { session_id, diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index 0cb448d8be..85243a641b 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::path::PathBuf; use anyhow::Context; use anyhow::Result; @@ -28,14 +27,11 @@ pub struct McpCli { pub config_overrides: CliConfigOverrides, #[command(subcommand)] - pub cmd: Option, + pub subcommand: McpSubcommand, } #[derive(Debug, clap::Subcommand)] pub enum McpSubcommand { - /// [experimental] Run the Codex MCP server (stdio transport). - Serve, - /// [experimental] List configured MCP servers. List(ListArgs), @@ -87,17 +83,13 @@ pub struct RemoveArgs { } impl McpCli { - pub async fn run(self, codex_linux_sandbox_exe: Option) -> Result<()> { + pub async fn run(self) -> Result<()> { let McpCli { config_overrides, - cmd, + subcommand, } = self; - let subcommand = cmd.unwrap_or(McpSubcommand::Serve); match subcommand { - McpSubcommand::Serve => { - codex_mcp_server::run_main(codex_linux_sandbox_exe, config_overrides).await?; - } McpSubcommand::List(args) => { run_list(&config_overrides, args)?; } diff --git a/codex-rs/docs/codex_mcp_interface.md b/codex-rs/docs/codex_mcp_interface.md index 8f0c279058..1291d3ee9f 100644 --- a/codex-rs/docs/codex_mcp_interface.md +++ b/codex-rs/docs/codex_mcp_interface.md @@ -3,7 +3,7 @@ This document describes Codex’s experimental MCP interface: a JSON‑RPC API that runs over the Model Context Protocol (MCP) transport to control a local Codex engine. - Status: experimental and subject to change without notice -- Server binary: `codex mcp` (or `codex-mcp-server`) +- Server binary: `codex mcp-server` (or `codex-mcp-server`) - Transport: standard MCP over stdio (JSON‑RPC 2.0, line‑delimited) ## Overview @@ -36,15 +36,17 @@ See code for full type definitions and exact shapes: `protocol/src/mcp_protocol. Run Codex as an MCP server and connect an MCP client: ```bash -codex mcp | your_mcp_client +codex mcp-server | your_mcp_client ``` For a simple inspection UI, you can also try: ```bash -npx @modelcontextprotocol/inspector codex mcp +npx @modelcontextprotocol/inspector codex mcp-server ``` +Use the separate `codex mcp` subcommand to manage configured MCP server launchers in `config.toml`. + ## Conversations Start a new session with optional overrides: diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index 9a5fd947d8..484af6d8e8 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -19,9 +19,8 @@ anyhow = { workspace = true } codex-arg0 = { workspace = true } codex-common = { workspace = true, features = ["cli"] } codex-core = { workspace = true } -codex-file-search = { workspace = true } -codex-login = { workspace = true } codex-protocol = { workspace = true } +codex-utils-json-to-toml = { workspace = true } mcp-types = { workspace = true } schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } @@ -34,14 +33,11 @@ tokio = { workspace = true, features = [ "rt-multi-thread", "signal", ] } -toml = { workspace = true } tracing = { workspace = true, features = ["log"] } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } -uuid = { workspace = true, features = ["serde", "v7"] } [dev-dependencies] assert_cmd = { workspace = true } -base64 = { workspace = true } core_test_support = { workspace = true } mcp_test_support = { workspace = true } os_info = { workspace = true } diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index d90924aa94..3ee1669020 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -2,6 +2,7 @@ use codex_core::protocol::AskForApproval; use codex_protocol::config_types::SandboxMode; +use codex_utils_json_to_toml::json_to_toml; use mcp_types::Tool; use mcp_types::ToolInputSchema; use schemars::JsonSchema; @@ -11,8 +12,6 @@ use serde::Serialize; use std::collections::HashMap; use std::path::PathBuf; -use crate::json_to_toml::json_to_toml; - /// Client-supplied configuration for a `codex` tool-call. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] #[serde(rename_all = "kebab-case")] diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs index 830bf49247..ffc4b3e362 100644 --- a/codex-rs/mcp-server/src/lib.rs +++ b/codex-rs/mcp-server/src/lib.rs @@ -20,13 +20,10 @@ use tracing::error; use tracing::info; use tracing_subscriber::EnvFilter; -mod codex_message_processor; mod codex_tool_config; mod codex_tool_runner; mod error_code; mod exec_approval; -mod fuzzy_file_search; -mod json_to_toml; pub(crate) mod message_processor; mod outgoing_message; mod patch_approval; diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 5868d60fc0..addc9572ec 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -1,14 +1,12 @@ use std::collections::HashMap; use std::path::PathBuf; -use crate::codex_message_processor::CodexMessageProcessor; use crate::codex_tool_config::CodexToolCallParam; use crate::codex_tool_config::CodexToolCallReplyParam; use crate::codex_tool_config::create_tool_for_codex_tool_call_param; use crate::codex_tool_config::create_tool_for_codex_tool_call_reply_param; use crate::error_code::INVALID_REQUEST_ERROR_CODE; use crate::outgoing_message::OutgoingMessageSender; -use codex_protocol::mcp_protocol::ClientRequest; use codex_protocol::mcp_protocol::ConversationId; use codex_core::AuthManager; @@ -38,7 +36,6 @@ use tokio::sync::Mutex; use tokio::task; pub(crate) struct MessageProcessor { - codex_message_processor: CodexMessageProcessor, outgoing: Arc, initialized: bool, codex_linux_sandbox_exe: Option, @@ -56,16 +53,8 @@ impl MessageProcessor { ) -> Self { let outgoing = Arc::new(outgoing); let auth_manager = AuthManager::shared(config.codex_home.clone()); - let conversation_manager = Arc::new(ConversationManager::new(auth_manager.clone())); - let codex_message_processor = CodexMessageProcessor::new( - auth_manager, - conversation_manager.clone(), - outgoing.clone(), - codex_linux_sandbox_exe.clone(), - config, - ); + let conversation_manager = Arc::new(ConversationManager::new(auth_manager)); Self { - codex_message_processor, outgoing, initialized: false, codex_linux_sandbox_exe, @@ -75,17 +64,6 @@ impl MessageProcessor { } pub(crate) async fn process_request(&mut self, request: JSONRPCRequest) { - if let Ok(request_json) = serde_json::to_value(request.clone()) - && let Ok(codex_request) = serde_json::from_value::(request_json) - { - // If the request is a Codex request, handle it with the Codex - // message processor. - self.codex_message_processor - .process_request(codex_request) - .await; - return; - } - // Hold on to the ID so we can respond. let request_id = request.id.clone(); diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index 6f376b1df5..2e350dd9a9 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -3,7 +3,6 @@ use std::sync::atomic::AtomicI64; use std::sync::atomic::Ordering; use codex_core::protocol::Event; -use codex_protocol::mcp_protocol::ServerNotification; use mcp_types::JSONRPC_VERSION; use mcp_types::JSONRPCError; use mcp_types::JSONRPCErrorError; @@ -125,12 +124,6 @@ impl OutgoingMessageSender { .await; } - pub(crate) async fn send_server_notification(&self, notification: ServerNotification) { - let _ = self - .sender - .send(OutgoingMessage::AppServerNotification(notification)); - } - pub(crate) async fn send_notification(&self, notification: OutgoingNotification) { let outgoing_message = OutgoingMessage::Notification(notification); let _ = self.sender.send(outgoing_message); @@ -146,9 +139,6 @@ impl OutgoingMessageSender { pub(crate) enum OutgoingMessage { Request(OutgoingRequest), Notification(OutgoingNotification), - /// AppServerNotification is specific to the case where this is run as an - /// "app server" as opposed to an MCP server. - AppServerNotification(ServerNotification), Response(OutgoingResponse), Error(OutgoingError), } @@ -172,21 +162,6 @@ impl From for JSONRPCMessage { params, }) } - AppServerNotification(notification) => { - let method = notification.to_string(); - let params = match notification.to_params() { - Ok(params) => Some(params), - Err(err) => { - warn!("failed to serialize notification params: {err}"); - None - } - }; - JSONRPCMessage::Notification(JSONRPCNotification { - jsonrpc: JSONRPC_VERSION.into(), - method, - params, - }) - } Response(OutgoingResponse { id, result }) => { JSONRPCMessage::Response(JSONRPCResponse { jsonrpc: JSONRPC_VERSION.into(), @@ -261,11 +236,9 @@ mod tests { use codex_core::protocol::SessionConfiguredEvent; use codex_protocol::config_types::ReasoningEffort; use codex_protocol::mcp_protocol::ConversationId; - use codex_protocol::mcp_protocol::LoginChatGptCompleteNotification; use pretty_assertions::assert_eq; use serde_json::json; use tempfile::NamedTempFile; - use uuid::Uuid; use super::*; @@ -357,29 +330,4 @@ mod tests { assert_eq!(params.unwrap(), expected_params); Ok(()) } - - #[test] - fn verify_server_notification_serialization() { - let notification = - ServerNotification::LoginChatGptComplete(LoginChatGptCompleteNotification { - login_id: Uuid::nil(), - success: true, - error: None, - }); - - let jsonrpc_notification: JSONRPCMessage = - OutgoingMessage::AppServerNotification(notification).into(); - assert_eq!( - JSONRPCMessage::Notification(JSONRPCNotification { - jsonrpc: "2.0".into(), - method: "loginChatGptComplete".into(), - params: Some(json!({ - "loginId": Uuid::nil(), - "success": true, - })), - }), - jsonrpc_notification, - "ensure the strum macros serialize the method field correctly" - ); - } } diff --git a/codex-rs/mcp-server/tests/common/Cargo.toml b/codex-rs/mcp-server/tests/common/Cargo.toml index e6f7117250..7c2bc2266a 100644 --- a/codex-rs/mcp-server/tests/common/Cargo.toml +++ b/codex-rs/mcp-server/tests/common/Cargo.toml @@ -11,7 +11,6 @@ anyhow = { workspace = true } assert_cmd = { workspace = true } codex-core = { workspace = true } codex-mcp-server = { workspace = true } -codex-protocol = { workspace = true } mcp-types = { workspace = true } os_info = { workspace = true } pretty_assertions = { workspace = true } diff --git a/codex-rs/mcp-server/tests/common/mcp_process.rs b/codex-rs/mcp-server/tests/common/mcp_process.rs index f89f72f5a4..a6bc966d7e 100644 --- a/codex-rs/mcp-server/tests/common/mcp_process.rs +++ b/codex-rs/mcp-server/tests/common/mcp_process.rs @@ -12,19 +12,6 @@ use tokio::process::ChildStdout; use anyhow::Context; use assert_cmd::prelude::*; use codex_mcp_server::CodexToolCallParam; -use codex_protocol::mcp_protocol::AddConversationListenerParams; -use codex_protocol::mcp_protocol::ArchiveConversationParams; -use codex_protocol::mcp_protocol::CancelLoginChatGptParams; -use codex_protocol::mcp_protocol::GetAuthStatusParams; -use codex_protocol::mcp_protocol::InterruptConversationParams; -use codex_protocol::mcp_protocol::ListConversationsParams; -use codex_protocol::mcp_protocol::LoginApiKeyParams; -use codex_protocol::mcp_protocol::NewConversationParams; -use codex_protocol::mcp_protocol::RemoveConversationListenerParams; -use codex_protocol::mcp_protocol::ResumeConversationParams; -use codex_protocol::mcp_protocol::SendUserMessageParams; -use codex_protocol::mcp_protocol::SendUserTurnParams; -use codex_protocol::mcp_protocol::SetDefaultModelParams; use mcp_types::CallToolRequestParams; use mcp_types::ClientCapabilities; @@ -213,167 +200,6 @@ impl McpProcess { .await } - /// Send a `newConversation` JSON-RPC request. - pub async fn send_new_conversation_request( - &mut self, - params: NewConversationParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("newConversation", params).await - } - - /// Send an `archiveConversation` JSON-RPC request. - pub async fn send_archive_conversation_request( - &mut self, - params: ArchiveConversationParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("archiveConversation", params).await - } - - /// Send an `addConversationListener` JSON-RPC request. - pub async fn send_add_conversation_listener_request( - &mut self, - params: AddConversationListenerParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("addConversationListener", params).await - } - - /// Send a `sendUserMessage` JSON-RPC request with a single text item. - pub async fn send_send_user_message_request( - &mut self, - params: SendUserMessageParams, - ) -> anyhow::Result { - // Wire format expects variants in camelCase; text item uses external tagging. - let params = Some(serde_json::to_value(params)?); - self.send_request("sendUserMessage", params).await - } - - /// Send a `removeConversationListener` JSON-RPC request. - pub async fn send_remove_conversation_listener_request( - &mut self, - params: RemoveConversationListenerParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("removeConversationListener", params) - .await - } - - /// Send a `sendUserTurn` JSON-RPC request. - pub async fn send_send_user_turn_request( - &mut self, - params: SendUserTurnParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("sendUserTurn", params).await - } - - /// Send a `interruptConversation` JSON-RPC request. - pub async fn send_interrupt_conversation_request( - &mut self, - params: InterruptConversationParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("interruptConversation", params).await - } - - /// Send a `getAuthStatus` JSON-RPC request. - pub async fn send_get_auth_status_request( - &mut self, - params: GetAuthStatusParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("getAuthStatus", params).await - } - - /// Send a `getUserSavedConfig` JSON-RPC request. - pub async fn send_get_user_saved_config_request(&mut self) -> anyhow::Result { - self.send_request("getUserSavedConfig", None).await - } - - /// Send a `getUserAgent` JSON-RPC request. - pub async fn send_get_user_agent_request(&mut self) -> anyhow::Result { - self.send_request("getUserAgent", None).await - } - - /// Send a `userInfo` JSON-RPC request. - pub async fn send_user_info_request(&mut self) -> anyhow::Result { - self.send_request("userInfo", None).await - } - - /// Send a `setDefaultModel` JSON-RPC request. - pub async fn send_set_default_model_request( - &mut self, - params: SetDefaultModelParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("setDefaultModel", params).await - } - - /// Send a `listConversations` JSON-RPC request. - pub async fn send_list_conversations_request( - &mut self, - params: ListConversationsParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("listConversations", params).await - } - - /// Send a `resumeConversation` JSON-RPC request. - pub async fn send_resume_conversation_request( - &mut self, - params: ResumeConversationParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("resumeConversation", params).await - } - - /// Send a `loginApiKey` JSON-RPC request. - pub async fn send_login_api_key_request( - &mut self, - params: LoginApiKeyParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("loginApiKey", params).await - } - - /// Send a `loginChatGpt` JSON-RPC request. - pub async fn send_login_chat_gpt_request(&mut self) -> anyhow::Result { - self.send_request("loginChatGpt", None).await - } - - /// Send a `cancelLoginChatGpt` JSON-RPC request. - pub async fn send_cancel_login_chat_gpt_request( - &mut self, - params: CancelLoginChatGptParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("cancelLoginChatGpt", params).await - } - - /// Send a `logoutChatGpt` JSON-RPC request. - pub async fn send_logout_chat_gpt_request(&mut self) -> anyhow::Result { - self.send_request("logoutChatGpt", None).await - } - - /// Send a `fuzzyFileSearch` JSON-RPC request. - pub async fn send_fuzzy_file_search_request( - &mut self, - query: &str, - roots: Vec, - cancellation_token: Option, - ) -> anyhow::Result { - let mut params = serde_json::json!({ - "query": query, - "roots": roots, - }); - if let Some(token) = cancellation_token { - params["cancellationToken"] = serde_json::json!(token); - } - self.send_request("fuzzyFileSearch", Some(params)).await - } - async fn send_request( &mut self, method: &str, @@ -471,58 +297,6 @@ impl McpProcess { } } - pub async fn read_stream_until_error_message( - &mut self, - request_id: RequestId, - ) -> anyhow::Result { - loop { - let message = self.read_jsonrpc_message().await?; - match message { - JSONRPCMessage::Notification(_) => { - eprintln!("notification: {message:?}"); - } - JSONRPCMessage::Request(_) => { - anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}"); - } - JSONRPCMessage::Response(_) => { - // Keep scanning; we're waiting for an error with matching id. - } - JSONRPCMessage::Error(err) => { - if err.id == request_id { - return Ok(err); - } - } - } - } - } - - pub async fn read_stream_until_notification_message( - &mut self, - method: &str, - ) -> anyhow::Result { - eprintln!("in read_stream_until_notification_message({method})"); - - loop { - let message = self.read_jsonrpc_message().await?; - match message { - JSONRPCMessage::Notification(notification) => { - if notification.method == method { - return Ok(notification); - } - } - JSONRPCMessage::Request(_) => { - anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}"); - } - JSONRPCMessage::Error(_) => { - anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}"); - } - JSONRPCMessage::Response(_) => { - anyhow::bail!("unexpected JSONRPCMessage::Response: {message:?}"); - } - } - } - } - /// Reads notifications until a legacy TaskComplete event is observed: /// Method "codex/event" with params.msg.type == "task_complete". pub async fn read_stream_until_legacy_task_complete_notification( diff --git a/codex-rs/mcp-server/tests/suite/mod.rs b/codex-rs/mcp-server/tests/suite/mod.rs index 4a3a91206f..6b50853b16 100644 --- a/codex-rs/mcp-server/tests/suite/mod.rs +++ b/codex-rs/mcp-server/tests/suite/mod.rs @@ -1,15 +1 @@ -// Aggregates all former standalone integration tests as modules. -mod archive_conversation; -mod auth; -mod codex_message_processor_flow; mod codex_tool; -mod config; -mod create_conversation; -mod fuzzy_file_search; -mod interrupt; -mod list_resume; -mod login; -mod send_message; -mod set_default_model; -mod user_agent; -mod user_info; diff --git a/codex-rs/protocol-ts/src/lib.rs b/codex-rs/protocol-ts/src/lib.rs index aaf820965d..12c0b7a1aa 100644 --- a/codex-rs/protocol-ts/src/lib.rs +++ b/codex-rs/protocol-ts/src/lib.rs @@ -21,6 +21,7 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { codex_protocol::mcp_protocol::InputItem::export_all_to(out_dir)?; codex_protocol::mcp_protocol::ClientRequest::export_all_to(out_dir)?; codex_protocol::mcp_protocol::ServerRequest::export_all_to(out_dir)?; + codex_protocol::mcp_protocol::InitializeResponse::export_all_to(out_dir)?; codex_protocol::mcp_protocol::NewConversationResponse::export_all_to(out_dir)?; codex_protocol::mcp_protocol::ListConversationsResponse::export_all_to(out_dir)?; codex_protocol::mcp_protocol::ResumeConversationResponse::export_all_to(out_dir)?; @@ -50,6 +51,7 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { // All notification types reachable from this enum will be generated by // induction, so they do not need to be listed individually. codex_protocol::mcp_protocol::ServerNotification::export_all_to(out_dir)?; + codex_protocol::mcp_protocol::ClientNotification::export_all_to(out_dir)?; generate_index_ts(out_dir)?; diff --git a/codex-rs/protocol/README.md b/codex-rs/protocol/README.md index 384d0b4859..7120d9f3b3 100644 --- a/codex-rs/protocol/README.md +++ b/codex-rs/protocol/README.md @@ -1,6 +1,6 @@ # codex-protocol -This crate defines the "types" for the protocol used by Codex CLI, which includes both "internal types" for communication between `codex-core` and `codex-tui`, as well as "external types" used with `codex mcp`. +This crate defines the "types" for the protocol used by Codex CLI, which includes both "internal types" for communication between `codex-core` and `codex-tui`, as well as "external types" used with `codex app-server`. This crate should have minimal dependencies. diff --git a/codex-rs/protocol/src/mcp_protocol.rs b/codex-rs/protocol/src/mcp_protocol.rs index ee6a56a86f..107055f6e0 100644 --- a/codex-rs/protocol/src/mcp_protocol.rs +++ b/codex-rs/protocol/src/mcp_protocol.rs @@ -12,6 +12,7 @@ use crate::protocol::FileChange; use crate::protocol::ReviewDecision; use crate::protocol::SandboxPolicy; use crate::protocol::TurnAbortReason; +use mcp_types::JSONRPCNotification; use mcp_types::RequestId; use serde::Deserialize; use serde::Serialize; @@ -92,6 +93,11 @@ pub enum AuthMode { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(tag = "method", rename_all = "camelCase")] pub enum ClientRequest { + Initialize { + #[serde(rename = "id")] + request_id: RequestId, + params: InitializeParams, + }, NewConversation { #[serde(rename = "id")] request_id: RequestId, @@ -197,6 +203,27 @@ pub enum ClientRequest { }, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)] +#[serde(rename_all = "camelCase")] +pub struct InitializeParams { + pub client_info: ClientInfo, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)] +#[serde(rename_all = "camelCase")] +pub struct ClientInfo { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + pub version: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct InitializeResponse { + pub user_agent: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)] #[serde(rename_all = "camelCase")] pub struct NewConversationParams { @@ -702,6 +729,33 @@ pub struct LoginChatGptCompleteNotification { pub error: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[serde(rename_all = "camelCase")] +pub struct SessionConfiguredNotification { + /// Name left as session_id instead of conversation_id for backwards compatibility. + pub session_id: ConversationId, + + /// Tell the client what model is being queried. + pub model: String, + + /// The effort the model is putting into reasoning about the user's request. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + + /// Identifier of the history log file (inode on Unix, 0 otherwise). + pub history_log_id: u64, + + /// Current number of entries in the history log. + pub history_entry_count: usize, + + /// Optional initial messages (as events) for resumed sessions. + /// When present, UIs can use these to seed the history. + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_messages: Option>, + + pub rollout_path: PathBuf, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct AuthStatusChangeNotification { @@ -710,7 +764,8 @@ pub struct AuthStatusChangeNotification { pub auth_method: Option, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS, Display)] +/// Notification sent from the server to the client. +#[derive(Serialize, Deserialize, Debug, Clone, TS, Display)] #[serde(tag = "method", content = "params", rename_all = "camelCase")] #[strum(serialize_all = "camelCase")] pub enum ServerNotification { @@ -719,6 +774,9 @@ pub enum ServerNotification { /// ChatGPT login flow completed LoginChatGptComplete(LoginChatGptCompleteNotification), + + /// The special session configured event for a new or resumed conversation. + SessionConfigured(SessionConfiguredNotification), } impl ServerNotification { @@ -726,10 +784,27 @@ impl ServerNotification { match self { ServerNotification::AuthStatusChange(params) => serde_json::to_value(params), ServerNotification::LoginChatGptComplete(params) => serde_json::to_value(params), + ServerNotification::SessionConfigured(params) => serde_json::to_value(params), } } } +impl TryFrom for ServerNotification { + type Error = serde_json::Error; + + fn try_from(value: JSONRPCNotification) -> Result { + serde_json::from_value(serde_json::to_value(value)?) + } +} + +/// Notification sent from the client to the server. +#[derive(Serialize, Deserialize, Debug, Clone, TS, Display)] +#[serde(tag = "method", content = "params", rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum ClientNotification { + Initialized, +} + #[cfg(test)] mod tests { use super::*; @@ -795,4 +870,17 @@ mod tests { ); Ok(()) } + + #[test] + fn serialize_client_notification() -> Result<()> { + let notification = ClientNotification::Initialized; + // Note there is no "params" field for this notification. + assert_eq!( + json!({ + "method": "initialized", + }), + serde_json::to_value(¬ification)?, + ); + Ok(()) + } } diff --git a/codex-rs/utils/json-to-toml/Cargo.toml b/codex-rs/utils/json-to-toml/Cargo.toml new file mode 100644 index 0000000000..a665724d78 --- /dev/null +++ b/codex-rs/utils/json-to-toml/Cargo.toml @@ -0,0 +1,14 @@ +[package] +edition.workspace = true +name = "codex-utils-json-to-toml" +version.workspace = true + +[dependencies] +serde_json = { workspace = true } +toml = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } + +[lints] +workspace = true diff --git a/codex-rs/mcp-server/src/json_to_toml.rs b/codex-rs/utils/json-to-toml/src/lib.rs similarity index 97% rename from codex-rs/mcp-server/src/json_to_toml.rs rename to codex-rs/utils/json-to-toml/src/lib.rs index 04f1f30615..43b7e06e06 100644 --- a/codex-rs/mcp-server/src/json_to_toml.rs +++ b/codex-rs/utils/json-to-toml/src/lib.rs @@ -2,7 +2,7 @@ use serde_json::Value as JsonValue; use toml::Value as TomlValue; /// Convert a `serde_json::Value` into a semantically equivalent `toml::Value`. -pub(crate) fn json_to_toml(v: JsonValue) -> TomlValue { +pub fn json_to_toml(v: JsonValue) -> TomlValue { match v { JsonValue::Null => TomlValue::String(String::new()), JsonValue::Bool(b) => TomlValue::Boolean(b), diff --git a/docs/advanced.md b/docs/advanced.md index 4edca7646f..869a5fb533 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -74,13 +74,13 @@ env = { "API_KEY" = "value" } ## Using Codex as an MCP Server -The Codex CLI can also be run as an MCP _server_ via `codex mcp`. For example, you can use `codex mcp` to make Codex available as a tool inside of a multi-agent framework like the OpenAI [Agents SDK](https://platform.openai.com/docs/guides/agents). +The Codex CLI can also be run as an MCP _server_ via `codex mcp-server`. For example, you can use `codex mcp-server` to make Codex available as a tool inside of a multi-agent framework like the OpenAI [Agents SDK](https://platform.openai.com/docs/guides/agents). Use `codex mcp` separately to add/list/get/remove MCP server launchers in your configuration. ### Codex MCP Server Quickstart You can launch a Codex MCP server with the [Model Context Protocol Inspector](https://modelcontextprotocol.io/legacy/tools/inspector): ``` bash -npx @modelcontextprotocol/inspector codex mcp +npx @modelcontextprotocol/inspector codex mcp-server ``` Send a `tools/list` request and you will see that there are two tools available: @@ -109,7 +109,7 @@ Property | Type | Description > [!TIP] > Codex often takes a few minutes to run. To accommodate this, adjust the MCP inspector's Request and Total timeouts to 600000ms (10 minutes) under ⛭ Configuration. -Use the MCP inspector and `codex mcp` to build a simple tic-tac-toe game with the following settings: +Use the MCP inspector and `codex mcp-server` to build a simple tic-tac-toe game with the following settings: **approval-policy:** never From 5b038135dead11f9aa44ecbe5341a859b5ceec69 Mon Sep 17 00:00:00 2001 From: easong-openai Date: Tue, 30 Sep 2025 03:10:33 -0700 Subject: [PATCH 23/23] Add cloud tasks (#3197) Adds a TUI for managing, applying, and creating cloud tasks --- codex-rs/Cargo.lock | 893 +++++---- codex-rs/Cargo.toml | 6 + codex-rs/backend-client/Cargo.toml | 18 + codex-rs/backend-client/src/client.rs | 244 +++ codex-rs/backend-client/src/lib.rs | 9 + codex-rs/backend-client/src/types.rs | 369 ++++ .../fixtures/task_details_with_diff.json | 38 + .../fixtures/task_details_with_error.json | 22 + codex-rs/chatgpt/Cargo.toml | 1 + codex-rs/chatgpt/src/apply_command.rs | 52 +- codex-rs/chatgpt/src/chatgpt_client.rs | 2 +- codex-rs/cli/Cargo.toml | 2 + codex-rs/cli/src/main.rs | 20 + codex-rs/cloud-tasks-client/Cargo.toml | 27 + codex-rs/cloud-tasks-client/src/api.rs | 158 ++ codex-rs/cloud-tasks-client/src/http.rs | 769 ++++++++ codex-rs/cloud-tasks-client/src/lib.rs | 29 + codex-rs/cloud-tasks-client/src/mock.rs | 180 ++ codex-rs/cloud-tasks/Cargo.toml | 36 + codex-rs/cloud-tasks/src/app.rs | 482 +++++ codex-rs/cloud-tasks/src/cli.rs | 9 + codex-rs/cloud-tasks/src/env_detect.rs | 361 ++++ codex-rs/cloud-tasks/src/lib.rs | 1631 +++++++++++++++++ codex-rs/cloud-tasks/src/new_task.rs | 35 + codex-rs/cloud-tasks/src/scrollable_diff.rs | 176 ++ codex-rs/cloud-tasks/src/ui.rs | 1048 +++++++++++ codex-rs/cloud-tasks/src/util.rs | 93 + codex-rs/cloud-tasks/tests/env_filter.rs | 22 + .../codex-backend-openapi-models/Cargo.toml | 17 + .../codex-backend-openapi-models/src/lib.rs | 6 + .../src/models/code_task_details_response.rs | 42 + .../models/external_pull_request_response.rs | 40 + .../src/models/git_pull_request.rs | 77 + .../src/models/mod.rs | 22 + .../models/paginated_list_task_list_item_.rs | 30 + .../src/models/task_list_item.rs | 63 + .../src/models/task_response.rs | 62 + codex-rs/core/src/config.rs | 2 +- codex-rs/core/src/mcp_connection_manager.rs | 3 +- codex-rs/git-apply/Cargo.toml | 17 + codex-rs/git-apply/src/lib.rs | 698 +++++++ codex-rs/mcp-client/src/mcp_client.rs | 3 +- codex-rs/protocol-ts/src/lib.rs | 2 +- codex-rs/tui/src/bottom_pane/chat_composer.rs | 49 +- codex-rs/tui/src/history_cell.rs | 11 +- codex-rs/tui/src/lib.rs | 4 + codex-rs/tui/src/markdown_render.rs | 2 +- .../tui/src/public_widgets/composer_input.rs | 128 ++ codex-rs/tui/src/public_widgets/mod.rs | 1 + 49 files changed, 7573 insertions(+), 438 deletions(-) create mode 100644 codex-rs/backend-client/Cargo.toml create mode 100644 codex-rs/backend-client/src/client.rs create mode 100644 codex-rs/backend-client/src/lib.rs create mode 100644 codex-rs/backend-client/src/types.rs create mode 100644 codex-rs/backend-client/tests/fixtures/task_details_with_diff.json create mode 100644 codex-rs/backend-client/tests/fixtures/task_details_with_error.json create mode 100644 codex-rs/cloud-tasks-client/Cargo.toml create mode 100644 codex-rs/cloud-tasks-client/src/api.rs create mode 100644 codex-rs/cloud-tasks-client/src/http.rs create mode 100644 codex-rs/cloud-tasks-client/src/lib.rs create mode 100644 codex-rs/cloud-tasks-client/src/mock.rs create mode 100644 codex-rs/cloud-tasks/Cargo.toml create mode 100644 codex-rs/cloud-tasks/src/app.rs create mode 100644 codex-rs/cloud-tasks/src/cli.rs create mode 100644 codex-rs/cloud-tasks/src/env_detect.rs create mode 100644 codex-rs/cloud-tasks/src/lib.rs create mode 100644 codex-rs/cloud-tasks/src/new_task.rs create mode 100644 codex-rs/cloud-tasks/src/scrollable_diff.rs create mode 100644 codex-rs/cloud-tasks/src/ui.rs create mode 100644 codex-rs/cloud-tasks/src/util.rs create mode 100644 codex-rs/cloud-tasks/tests/env_filter.rs create mode 100644 codex-rs/codex-backend-openapi-models/Cargo.toml create mode 100644 codex-rs/codex-backend-openapi-models/src/lib.rs create mode 100644 codex-rs/codex-backend-openapi-models/src/models/code_task_details_response.rs create mode 100644 codex-rs/codex-backend-openapi-models/src/models/external_pull_request_response.rs create mode 100644 codex-rs/codex-backend-openapi-models/src/models/git_pull_request.rs create mode 100644 codex-rs/codex-backend-openapi-models/src/models/mod.rs create mode 100644 codex-rs/codex-backend-openapi-models/src/models/paginated_list_task_list_item_.rs create mode 100644 codex-rs/codex-backend-openapi-models/src/models/task_list_item.rs create mode 100644 codex-rs/codex-backend-openapi-models/src/models/task_response.rs create mode 100644 codex-rs/git-apply/Cargo.toml create mode 100644 codex-rs/git-apply/src/lib.rs create mode 100644 codex-rs/tui/src/public_widgets/composer_input.rs create mode 100644 codex-rs/tui/src/public_widgets/mod.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 6707e75bbb..49f4a148a8 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -69,7 +69,7 @@ checksum = "fe233a377643e0fc1a56421d7c90acdec45c291b30345eb9f08e8d0ddce5a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -141,29 +141,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "app_test_support" @@ -181,9 +181,9 @@ dependencies = [ [[package]] name = "arboard" -version = "3.6.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ "clipboard-win", "image", @@ -195,7 +195,7 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "x11rb", ] @@ -246,7 +246,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -321,7 +321,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -332,7 +332,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -349,9 +349,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "98e529aee37b5c8206bb4bf4c44797127566d72f76952c970bd3d1e85de8f4e2" dependencies = [ "axum-core", "bytes", @@ -367,8 +367,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "sync_wrapper", "tokio", "tower", @@ -378,9 +377,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "0ac7a6beb1182c7e30253ee75c3e918080bfb83f5a3023bcdf7209d85fd147e6" dependencies = [ "bytes", "futures-core", @@ -389,7 +388,6 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -397,9 +395,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -407,7 +405,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link 0.2.0", ] [[package]] @@ -454,9 +452,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "block-buffer" @@ -486,9 +484,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" [[package]] name = "byteorder" @@ -525,10 +523,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.30" +version = "1.2.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -540,9 +539,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -578,9 +577,9 @@ checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" [[package]] name = "clap" -version = "4.5.47" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", @@ -588,9 +587,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.47" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -601,9 +600,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.57" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" +checksum = "75bf0b32ad2e152de789bb635ea4d3078f6b838ad7974143e99b99f45a04af4a" dependencies = [ "clap", ] @@ -617,7 +616,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -707,6 +706,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "codex-backend-client" +version = "0.0.0" +dependencies = [ + "anyhow", + "codex-backend-openapi-models", + "pretty_assertions", + "reqwest", + "serde", + "serde_json", +] + +[[package]] +name = "codex-backend-openapi-models" +version = "0.0.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "codex-chatgpt" version = "0.0.0" @@ -715,6 +734,7 @@ dependencies = [ "clap", "codex-common", "codex-core", + "codex-git-apply", "serde", "serde_json", "tempfile", @@ -732,6 +752,7 @@ dependencies = [ "codex-app-server", "codex-arg0", "codex-chatgpt", + "codex-cloud-tasks", "codex-common", "codex-core", "codex-exec", @@ -740,6 +761,7 @@ dependencies = [ "codex-process-hardening", "codex-protocol", "codex-protocol-ts", + "codex-responses-api-proxy", "codex-tui", "ctor 0.5.0", "owo-colors", @@ -753,6 +775,48 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "codex-cloud-tasks" +version = "0.0.0" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "chrono", + "clap", + "codex-cloud-tasks-client", + "codex-common", + "codex-core", + "codex-login", + "codex-tui", + "crossterm", + "ratatui", + "reqwest", + "serde", + "serde_json", + "throbber-widgets-tui", + "tokio", + "tokio-stream", + "tracing", + "tracing-subscriber", + "unicode-width 0.1.14", +] + +[[package]] +name = "codex-cloud-tasks-client" +version = "0.0.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "codex-backend-client", + "codex-git-apply", + "diffy", + "serde", + "serde_json", + "thiserror 2.0.16", +] + [[package]] name = "codex-common" version = "0.0.0" @@ -788,7 +852,7 @@ dependencies = [ "escargot", "eventsource-stream", "futures", - "indexmap 2.10.0", + "indexmap 2.11.4", "landlock", "libc", "maplit", @@ -798,7 +862,7 @@ dependencies = [ "portable-pty", "predicates", "pretty_assertions", - "rand", + "rand 0.9.2", "regex-lite", "reqwest", "seccompiler", @@ -893,6 +957,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "codex-git-apply" +version = "0.0.0" +dependencies = [ + "once_cell", + "regex", + "tempfile", +] + [[package]] name = "codex-git-tooling" version = "0.0.0" @@ -926,7 +999,7 @@ dependencies = [ "codex-core", "codex-protocol", "core_test_support", - "rand", + "rand 0.9.2", "reqwest", "serde", "serde_json", @@ -1123,7 +1196,7 @@ dependencies = [ "pathdiff", "pretty_assertions", "pulldown-cmark", - "rand", + "rand 0.9.2", "ratatui", "regex-lite", "serde", @@ -1348,7 +1421,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "crossterm_winapi", "futures-core", "mio", @@ -1441,7 +1514,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1455,7 +1528,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1466,7 +1539,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1477,7 +1550,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1557,7 +1630,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "unicode-xid", ] @@ -1569,7 +1642,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "unicode-xid", ] @@ -1631,8 +1704,8 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.0", - "windows-sys 0.60.2", + "redox_users 0.5.2", + "windows-sys 0.61.1", ] [[package]] @@ -1652,7 +1725,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "objc2", ] @@ -1674,7 +1747,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1727,14 +1800,14 @@ checksum = "83e195b4945e88836d826124af44fdcb262ec01ef94d44f14f4fb5103f19892a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "dyn-clone" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" @@ -1789,7 +1862,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1838,12 +1911,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.1", ] [[package]] @@ -1865,9 +1938,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -1928,7 +2001,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1938,7 +2011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix 1.0.8", + "rustix 1.1.2", "windows-sys 0.59.0", ] @@ -1962,6 +2035,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + [[package]] name = "fixed_decimal" version = "0.7.0" @@ -2027,9 +2106,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -2090,7 +2169,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2144,19 +2223,19 @@ dependencies = [ [[package]] name = "gethostname" -version = "0.4.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" dependencies = [ - "libc", - "windows-targets 0.48.5", + "rustix 1.1.2", + "windows-targets 0.52.6", ] [[package]] name = "getopts" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ "unicode-width 0.2.1", ] @@ -2184,15 +2263,15 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "globset" @@ -2204,14 +2283,14 @@ dependencies = [ "bstr", "log", "regex-automata", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", ] [[package]] name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -2219,7 +2298,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.10.0", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", @@ -2254,15 +2333,21 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "heck" version = "0.5.0" @@ -2417,9 +2502,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64", "bytes", @@ -2443,9 +2528,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2453,7 +2538,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.1", ] [[package]] @@ -2604,9 +2689,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -2657,9 +2742,9 @@ dependencies = [ [[package]] name = "indenter" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" @@ -2674,13 +2759,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.0", "serde", + "serde_core", ] [[package]] @@ -2710,25 +2796,25 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "inventory" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" dependencies = [ "rustversion", ] [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -2826,7 +2912,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2853,9 +2939,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -2894,9 +2980,9 @@ dependencies = [ [[package]] name = "landlock" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d2ef408b88e913bfc6594f5e693d57676f6463ded7d8bf994175364320c706" +checksum = "affe8b77dce5b172f8e290bd801b12832a77cd1942d1ea98259916e89d5829d6" dependencies = [ "enumflags2", "libc", @@ -2911,9 +2997,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libm" @@ -2923,11 +3009,11 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.6" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "libc", ] @@ -2939,9 +3025,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -2994,7 +3080,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -3065,9 +3151,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" @@ -3185,7 +3271,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -3197,7 +3283,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -3306,7 +3392,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "objc2", "objc2-core-graphics", "objc2-foundation", @@ -3318,7 +3404,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "dispatch2", "objc2", ] @@ -3329,7 +3415,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "dispatch2", "objc2", "objc2-core-foundation", @@ -3348,7 +3434,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "objc2", "objc2-core-foundation", ] @@ -3359,16 +3445,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "objc2", "objc2-core-foundation", ] [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -3391,7 +3477,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -3408,7 +3494,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3419,9 +3505,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.5.1+3.5.1" +version = "300.5.2+3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "735230c832b28c000e3bc117119e6466a663ec73506bc0a9907ea4187508e42a" +checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" dependencies = [ "cc", ] @@ -3530,7 +3616,7 @@ dependencies = [ "futures-util", "opentelemetry", "percent-encoding", - "rand", + "rand 0.9.2", "serde_json", "thiserror 2.0.16", "tokio", @@ -3628,9 +3714,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" @@ -3639,7 +3725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.10.0", + "indexmap 2.11.4", ] [[package]] @@ -3668,7 +3754,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3691,12 +3777,12 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" -version = "1.7.4" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64", - "indexmap 2.10.0", + "indexmap 2.11.4", "quick-xml", "serde", "time", @@ -3708,7 +3794,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "crc32fast", "fdeflate", "flate2", @@ -3753,9 +3839,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "serde", "zerovec", @@ -3824,9 +3910,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -3838,7 +3924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3ef4f2f0422f23a82ec9f628ea2acd12871c81a9362b02c43c1aa86acfc3ba1" dependencies = [ "futures", - "indexmap 2.10.0", + "indexmap 2.11.4", "nix 0.30.1", "tokio", "tracing", @@ -3865,7 +3951,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3874,7 +3960,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "getopts", "memchr", "pulldown-cmark-escape", @@ -3889,9 +3975,9 @@ checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3" [[package]] name = "pxfm" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55f4fedc84ed39cb7a489322318976425e42a147e2be79d8f878e2884f94e84" +checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" dependencies = [ "num-traits", ] @@ -3904,9 +3990,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.0" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", ] @@ -3940,7 +4026,7 @@ dependencies = [ "bytes", "getrandom 0.3.3", "lru-slab", - "rand", + "rand 0.9.2", "ring", "rustc-hash", "rustls", @@ -3991,14 +4077,35 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -4008,7 +4115,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", ] [[package]] @@ -4025,7 +4141,7 @@ name = "ratatui" version = "0.29.0" source = "git+https://github.com/nornagon/ratatui?branch=nornagon-v0.29.0-patch#9b2ad1298408c45918ee9f8241a6f95498cdbed2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cassowary", "compact_str", "crossterm", @@ -4042,11 +4158,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.15" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", ] [[package]] @@ -4062,9 +4178,9 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", @@ -4088,30 +4204,30 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", ] [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", ] [[package]] @@ -4128,9 +4244,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "reqwest" @@ -4211,7 +4327,7 @@ dependencies = [ "paste", "pin-project-lite", "process-wrap", - "rand", + "rand 0.9.2", "reqwest", "rmcp-macros", "schemars 1.0.4", @@ -4237,14 +4353,14 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -4258,7 +4374,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4267,22 +4383,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.1", ] [[package]] name = "rustls" -version = "0.23.29" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "once_cell", "ring", @@ -4316,9 +4432,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "ring", "rustls-pki-types", @@ -4327,9 +4443,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustyline" @@ -4337,7 +4453,7 @@ version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "clipboard-win", "fd-lock", @@ -4370,11 +4486,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -4466,7 +4582,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4478,7 +4594,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4502,7 +4618,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -4515,7 +4631,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4524,9 +4640,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -4534,9 +4650,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.226" +version = "1.0.227" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +checksum = "80ece43fc6fbed4eb5392ab50c07334d3e577cbf40997ee896fe7af40bba4245" dependencies = [ "serde_core", "serde_derive", @@ -4544,22 +4660,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.226" +version = "1.0.227" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" +checksum = "7a576275b607a2c86ea29e410193df32bc680303c82f31e275bbfcafe8b33be5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.226" +version = "1.0.227" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" +checksum = "51e694923b8824cf0e9b382adf0f60d4e05f348f357b38833a3fa5ed7c2ede04" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4570,7 +4686,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4579,7 +4695,7 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.4", "itoa", "memchr", "ryu", @@ -4595,16 +4711,16 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "serde_spanned" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -4621,15 +4737,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" dependencies = [ "base64", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.11.4", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -4641,21 +4757,21 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" dependencies = [ - "darling 0.20.11", + "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "serial2" -version = "0.2.31" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26e1e5956803a69ddd72ce2de337b577898801528749565def03515f82bad5bb" +checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c" dependencies = [ "cfg-if", "libc", @@ -4738,9 +4854,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -4875,7 +4991,7 @@ dependencies = [ "dupe", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4977,7 +5093,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4989,7 +5105,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5020,9 +5136,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -5046,7 +5162,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5064,7 +5180,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5088,8 +5204,8 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.8", - "windows-sys 0.60.2", + "rustix 1.1.2", + "windows-sys 0.61.1", ] [[package]] @@ -5114,12 +5230,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix 1.0.8", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.60.2", ] [[package]] @@ -5174,7 +5290,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5185,7 +5301,7 @@ checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5197,6 +5313,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "throbber-widgets-tui" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d36b5738d666a2b4c91b7c24998a8588db724b3107258343ebf8824bf55b06d" +dependencies = [ + "rand 0.8.5", + "ratatui", +] + [[package]] name = "tiff" version = "0.10.3" @@ -5318,7 +5444,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5333,9 +5459,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -5380,12 +5506,12 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.5" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" dependencies = [ - "indexmap 2.10.0", - "serde", + "indexmap 2.11.4", + "serde_core", "serde_spanned", "toml_datetime", "toml_parser", @@ -5395,20 +5521,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" dependencies = [ - "serde", + "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.4" +version = "0.23.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.4", "toml_datetime", "toml_parser", "toml_writer", @@ -5417,18 +5543,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" [[package]] name = "tonic" @@ -5467,7 +5593,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.10.0", + "indexmap 2.11.4", "pin-project-lite", "slab", "sync_wrapper", @@ -5484,7 +5610,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "bytes", "futures-util", "http", @@ -5540,7 +5666,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5610,18 +5736,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "tree-sitter" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd2a058a86cfece0bf96f7cce1021efef9c8ed0e892ab74639173e5ed7a34fa" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" dependencies = [ "cc", "regex", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", "serde_json", "streaming-iterator", "tree-sitter-language", @@ -5669,7 +5795,7 @@ checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "termcolor", ] @@ -5687,9 +5813,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-linebreak" @@ -5740,9 +5866,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -5855,44 +5981,54 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "wit-bindgen-rt", + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -5903,9 +6039,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5913,22 +6049,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] @@ -5948,9 +6084,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", @@ -6033,11 +6169,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -6053,7 +6189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.61.2", "windows-future", "windows-link 0.1.3", "windows-numerics", @@ -6065,7 +6201,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -6077,8 +6213,21 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", ] [[package]] @@ -6087,31 +6236,31 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", "windows-threading", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -6132,7 +6281,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", ] @@ -6143,8 +6292,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -6156,6 +6305,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -6165,6 +6323,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -6198,7 +6365,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.4", +] + +[[package]] +name = "windows-sys" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -6216,21 +6392,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -6249,10 +6410,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" dependencies = [ + "windows-link 0.2.0", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -6278,12 +6440,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6302,12 +6458,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6326,12 +6476,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6362,12 +6506,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6386,12 +6524,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6410,12 +6542,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6434,12 +6560,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6454,9 +6574,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -6500,13 +6620,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -6516,20 +6633,20 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "x11rb" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ "gethostname", - "rustix 0.38.44", + "rustix 1.1.2", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" [[package]] name = "yansi" @@ -6557,28 +6674,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -6598,7 +6715,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -6621,9 +6738,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -6638,7 +6755,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -6649,9 +6766,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" -version = "0.4.19" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ "zune-core", ] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 7946f1d0e7..66d8073ca3 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -1,9 +1,13 @@ [workspace] members = [ + "backend-client", "ansi-escape", "app-server", "apply-patch", "arg0", + "codex-backend-openapi-models", + "cloud-tasks", + "cloud-tasks-client", "cli", "common", "core", @@ -24,6 +28,7 @@ members = [ "responses-api-proxy", "otel", "tui", + "git-apply", "utils/json-to-toml", "utils/readiness", ] @@ -59,6 +64,7 @@ codex-otel = { path = "otel" } codex-process-hardening = { path = "process-hardening" } codex-protocol = { path = "protocol" } codex-protocol-ts = { path = "protocol-ts" } +codex-responses-api-proxy = { path = "responses-api-proxy" } codex-rmcp-client = { path = "rmcp-client" } codex-tui = { path = "tui" } codex-utils-json-to-toml = { path = "utils/json-to-toml" } diff --git a/codex-rs/backend-client/Cargo.toml b/codex-rs/backend-client/Cargo.toml new file mode 100644 index 0000000000..a8e2ee85fb --- /dev/null +++ b/codex-rs/backend-client/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "codex-backend-client" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +path = "src/lib.rs" + +[dependencies] +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +codex-backend-openapi-models = { path = "../codex-backend-openapi-models" } + +[dev-dependencies] +pretty_assertions = "1" diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs new file mode 100644 index 0000000000..06ad00cb80 --- /dev/null +++ b/codex-rs/backend-client/src/client.rs @@ -0,0 +1,244 @@ +use crate::types::CodeTaskDetailsResponse; +use crate::types::PaginatedListTaskListItem; +use crate::types::TurnAttemptsSiblingTurnsResponse; +use anyhow::Result; +use reqwest::header::AUTHORIZATION; +use reqwest::header::CONTENT_TYPE; +use reqwest::header::HeaderMap; +use reqwest::header::HeaderName; +use reqwest::header::HeaderValue; +use reqwest::header::USER_AGENT; +use serde::de::DeserializeOwned; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PathStyle { + /// /api/codex/… + CodexApi, + /// /wham/… + ChatGptApi, +} + +impl PathStyle { + pub fn from_base_url(base_url: &str) -> Self { + if base_url.contains("/backend-api") { + PathStyle::ChatGptApi + } else { + PathStyle::CodexApi + } + } +} + +#[derive(Clone, Debug)] +pub struct Client { + base_url: String, + http: reqwest::Client, + bearer_token: Option, + user_agent: Option, + chatgpt_account_id: Option, + path_style: PathStyle, +} + +impl Client { + pub fn new(base_url: impl Into) -> Result { + let mut base_url = base_url.into(); + // Normalize common ChatGPT hostnames to include /backend-api so we hit the WHAM paths. + // Also trim trailing slashes for consistent URL building. + while base_url.ends_with('/') { + base_url.pop(); + } + if (base_url.starts_with("https://chatgpt.com") + || base_url.starts_with("https://chat.openai.com")) + && !base_url.contains("/backend-api") + { + base_url = format!("{base_url}/backend-api"); + } + let http = reqwest::Client::builder().build()?; + let path_style = PathStyle::from_base_url(&base_url); + Ok(Self { + base_url, + http, + bearer_token: None, + user_agent: None, + chatgpt_account_id: None, + path_style, + }) + } + + pub fn with_bearer_token(mut self, token: impl Into) -> Self { + self.bearer_token = Some(token.into()); + self + } + + pub fn with_user_agent(mut self, ua: impl Into) -> Self { + if let Ok(hv) = HeaderValue::from_str(&ua.into()) { + self.user_agent = Some(hv); + } + self + } + + pub fn with_chatgpt_account_id(mut self, account_id: impl Into) -> Self { + self.chatgpt_account_id = Some(account_id.into()); + self + } + + pub fn with_path_style(mut self, style: PathStyle) -> Self { + self.path_style = style; + self + } + + fn headers(&self) -> HeaderMap { + let mut h = HeaderMap::new(); + if let Some(ua) = &self.user_agent { + h.insert(USER_AGENT, ua.clone()); + } else { + h.insert(USER_AGENT, HeaderValue::from_static("codex-cli")); + } + if let Some(token) = &self.bearer_token { + let value = format!("Bearer {token}"); + if let Ok(hv) = HeaderValue::from_str(&value) { + h.insert(AUTHORIZATION, hv); + } + } + if let Some(acc) = &self.chatgpt_account_id + && let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id") + && let Ok(hv) = HeaderValue::from_str(acc) + { + h.insert(name, hv); + } + h + } + + async fn exec_request( + &self, + req: reqwest::RequestBuilder, + method: &str, + url: &str, + ) -> Result<(String, String)> { + let res = req.send().await?; + let status = res.status(); + let ct = res + .headers() + .get(CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + let body = res.text().await.unwrap_or_default(); + if !status.is_success() { + anyhow::bail!("{method} {url} failed: {status}; content-type={ct}; body={body}"); + } + Ok((body, ct)) + } + + fn decode_json(&self, url: &str, ct: &str, body: &str) -> Result { + match serde_json::from_str::(body) { + Ok(v) => Ok(v), + Err(e) => { + anyhow::bail!("Decode error for {url}: {e}; content-type={ct}; body={body}"); + } + } + } + + pub async fn list_tasks( + &self, + limit: Option, + task_filter: Option<&str>, + environment_id: Option<&str>, + ) -> Result { + let url = match self.path_style { + PathStyle::CodexApi => format!("{}/api/codex/tasks/list", self.base_url), + PathStyle::ChatGptApi => format!("{}/wham/tasks/list", self.base_url), + }; + let req = self.http.get(&url).headers(self.headers()); + let req = if let Some(lim) = limit { + req.query(&[("limit", lim)]) + } else { + req + }; + let req = if let Some(tf) = task_filter { + req.query(&[("task_filter", tf)]) + } else { + req + }; + let req = if let Some(id) = environment_id { + req.query(&[("environment_id", id)]) + } else { + req + }; + let (body, ct) = self.exec_request(req, "GET", &url).await?; + self.decode_json::(&url, &ct, &body) + } + + pub async fn get_task_details(&self, task_id: &str) -> Result { + let (parsed, _body, _ct) = self.get_task_details_with_body(task_id).await?; + Ok(parsed) + } + + pub async fn get_task_details_with_body( + &self, + task_id: &str, + ) -> Result<(CodeTaskDetailsResponse, String, String)> { + let url = match self.path_style { + PathStyle::CodexApi => format!("{}/api/codex/tasks/{}", self.base_url, task_id), + PathStyle::ChatGptApi => format!("{}/wham/tasks/{}", self.base_url, task_id), + }; + let req = self.http.get(&url).headers(self.headers()); + let (body, ct) = self.exec_request(req, "GET", &url).await?; + let parsed: CodeTaskDetailsResponse = self.decode_json(&url, &ct, &body)?; + Ok((parsed, body, ct)) + } + + pub async fn list_sibling_turns( + &self, + task_id: &str, + turn_id: &str, + ) -> Result { + let url = match self.path_style { + PathStyle::CodexApi => format!( + "{}/api/codex/tasks/{}/turns/{}/sibling_turns", + self.base_url, task_id, turn_id + ), + PathStyle::ChatGptApi => format!( + "{}/wham/tasks/{}/turns/{}/sibling_turns", + self.base_url, task_id, turn_id + ), + }; + let req = self.http.get(&url).headers(self.headers()); + let (body, ct) = self.exec_request(req, "GET", &url).await?; + self.decode_json::(&url, &ct, &body) + } + + /// Create a new task (user turn) by POSTing to the appropriate backend path + /// based on `path_style`. Returns the created task id. + pub async fn create_task(&self, request_body: serde_json::Value) -> Result { + let url = match self.path_style { + PathStyle::CodexApi => format!("{}/api/codex/tasks", self.base_url), + PathStyle::ChatGptApi => format!("{}/wham/tasks", self.base_url), + }; + let req = self + .http + .post(&url) + .headers(self.headers()) + .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) + .json(&request_body); + let (body, ct) = self.exec_request(req, "POST", &url).await?; + // Extract id from JSON: prefer `task.id`; fallback to top-level `id` when present. + match serde_json::from_str::(&body) { + Ok(v) => { + if let Some(id) = v + .get("task") + .and_then(|t| t.get("id")) + .and_then(|s| s.as_str()) + { + Ok(id.to_string()) + } else if let Some(id) = v.get("id").and_then(|s| s.as_str()) { + Ok(id.to_string()) + } else { + anyhow::bail!( + "POST {url} succeeded but no task id found; content-type={ct}; body={body}" + ); + } + } + Err(e) => anyhow::bail!("Decode error for {url}: {e}; content-type={ct}; body={body}"), + } + } +} diff --git a/codex-rs/backend-client/src/lib.rs b/codex-rs/backend-client/src/lib.rs new file mode 100644 index 0000000000..29fe9f3c6b --- /dev/null +++ b/codex-rs/backend-client/src/lib.rs @@ -0,0 +1,9 @@ +mod client; +pub mod types; + +pub use client::Client; +pub use types::CodeTaskDetailsResponse; +pub use types::CodeTaskDetailsResponseExt; +pub use types::PaginatedListTaskListItem; +pub use types::TaskListItem; +pub use types::TurnAttemptsSiblingTurnsResponse; diff --git a/codex-rs/backend-client/src/types.rs b/codex-rs/backend-client/src/types.rs new file mode 100644 index 0000000000..8e2dfb2126 --- /dev/null +++ b/codex-rs/backend-client/src/types.rs @@ -0,0 +1,369 @@ +pub use codex_backend_openapi_models::models::PaginatedListTaskListItem; +pub use codex_backend_openapi_models::models::TaskListItem; + +use serde::Deserialize; +use serde::de::Deserializer; +use serde_json::Value; +use std::collections::HashMap; + +/// Hand-rolled models for the Cloud Tasks task-details response. +/// The generated OpenAPI models are pretty bad. This is a half-step +/// towards hand-rolling them. +#[derive(Clone, Debug, Deserialize)] +pub struct CodeTaskDetailsResponse { + #[serde(default)] + pub current_user_turn: Option, + #[serde(default)] + pub current_assistant_turn: Option, + #[serde(default)] + pub current_diff_task_turn: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct Turn { + #[serde(default)] + pub id: Option, + #[serde(default)] + pub attempt_placement: Option, + #[serde(default, rename = "turn_status")] + pub turn_status: Option, + #[serde(default, deserialize_with = "deserialize_vec")] + pub sibling_turn_ids: Vec, + #[serde(default, deserialize_with = "deserialize_vec")] + pub input_items: Vec, + #[serde(default, deserialize_with = "deserialize_vec")] + pub output_items: Vec, + #[serde(default)] + pub worklog: Option, + #[serde(default)] + pub error: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct TurnItem { + #[serde(rename = "type", default)] + pub kind: String, + #[serde(default)] + pub role: Option, + #[serde(default, deserialize_with = "deserialize_vec")] + pub content: Vec, + #[serde(default)] + pub diff: Option, + #[serde(default)] + pub output_diff: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub enum ContentFragment { + Structured(StructuredContent), + Text(String), +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct StructuredContent { + #[serde(rename = "content_type", default)] + pub content_type: Option, + #[serde(default)] + pub text: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct DiffPayload { + #[serde(default)] + pub diff: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct Worklog { + #[serde(default, deserialize_with = "deserialize_vec")] + pub messages: Vec, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct WorklogMessage { + #[serde(default)] + pub author: Option, + #[serde(default)] + pub content: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct Author { + #[serde(default)] + pub role: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct WorklogContent { + #[serde(default)] + pub parts: Vec, +} + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct TurnError { + #[serde(default)] + pub code: Option, + #[serde(default)] + pub message: Option, +} + +impl ContentFragment { + fn text(&self) -> Option<&str> { + match self { + ContentFragment::Structured(inner) => { + if inner + .content_type + .as_deref() + .map(|ct| ct.eq_ignore_ascii_case("text")) + .unwrap_or(false) + { + inner.text.as_deref().filter(|s| !s.is_empty()) + } else { + None + } + } + ContentFragment::Text(raw) => { + if raw.trim().is_empty() { + None + } else { + Some(raw.as_str()) + } + } + } + } +} + +impl TurnItem { + fn text_values(&self) -> Vec { + self.content + .iter() + .filter_map(|fragment| fragment.text().map(str::to_string)) + .collect() + } + + fn diff_text(&self) -> Option { + if self.kind == "output_diff" { + if let Some(diff) = &self.diff + && !diff.is_empty() + { + return Some(diff.clone()); + } + } else if self.kind == "pr" + && let Some(payload) = &self.output_diff + && let Some(diff) = &payload.diff + && !diff.is_empty() + { + return Some(diff.clone()); + } + None + } +} + +impl Turn { + fn unified_diff(&self) -> Option { + self.output_items.iter().find_map(TurnItem::diff_text) + } + + fn message_texts(&self) -> Vec { + let mut out: Vec = self + .output_items + .iter() + .filter(|item| item.kind == "message") + .flat_map(TurnItem::text_values) + .collect(); + + if let Some(log) = &self.worklog { + for message in &log.messages { + if message.is_assistant() { + out.extend(message.text_values()); + } + } + } + + out + } + + fn user_prompt(&self) -> Option { + let parts: Vec = self + .input_items + .iter() + .filter(|item| item.kind == "message") + .filter(|item| { + item.role + .as_deref() + .map(|r| r.eq_ignore_ascii_case("user")) + .unwrap_or(true) + }) + .flat_map(TurnItem::text_values) + .collect(); + + if parts.is_empty() { + None + } else { + Some(parts.join( + " + +", + )) + } + } + + fn error_summary(&self) -> Option { + self.error.as_ref().and_then(TurnError::summary) + } +} + +impl WorklogMessage { + fn is_assistant(&self) -> bool { + self.author + .as_ref() + .and_then(|a| a.role.as_deref()) + .map(|role| role.eq_ignore_ascii_case("assistant")) + .unwrap_or(false) + } + + fn text_values(&self) -> Vec { + self.content + .as_ref() + .map(|content| { + content + .parts + .iter() + .filter_map(|fragment| fragment.text().map(str::to_string)) + .collect() + }) + .unwrap_or_default() + } +} + +impl TurnError { + fn summary(&self) -> Option { + let code = self.code.as_deref().unwrap_or(""); + let message = self.message.as_deref().unwrap_or(""); + match (code.is_empty(), message.is_empty()) { + (true, true) => None, + (false, true) => Some(code.to_string()), + (true, false) => Some(message.to_string()), + (false, false) => Some(format!("{code}: {message}")), + } + } +} + +pub trait CodeTaskDetailsResponseExt { + /// Attempt to extract a unified diff string from the assistant or diff turn. + fn unified_diff(&self) -> Option; + /// Extract assistant text output messages (no diff) from current turns. + fn assistant_text_messages(&self) -> Vec; + /// Extract the user's prompt text from the current user turn, when present. + fn user_text_prompt(&self) -> Option; + /// Extract an assistant error message (if the turn failed and provided one). + fn assistant_error_message(&self) -> Option; +} + +impl CodeTaskDetailsResponseExt for CodeTaskDetailsResponse { + fn unified_diff(&self) -> Option { + [ + self.current_diff_task_turn.as_ref(), + self.current_assistant_turn.as_ref(), + ] + .into_iter() + .flatten() + .find_map(Turn::unified_diff) + } + + fn assistant_text_messages(&self) -> Vec { + let mut out = Vec::new(); + for turn in [ + self.current_diff_task_turn.as_ref(), + self.current_assistant_turn.as_ref(), + ] + .into_iter() + .flatten() + { + out.extend(turn.message_texts()); + } + out + } + + fn user_text_prompt(&self) -> Option { + self.current_user_turn.as_ref().and_then(Turn::user_prompt) + } + + fn assistant_error_message(&self) -> Option { + self.current_assistant_turn + .as_ref() + .and_then(Turn::error_summary) + } +} + +fn deserialize_vec<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + Option::>::deserialize(deserializer).map(|opt| opt.unwrap_or_default()) +} + +#[derive(Clone, Debug, Deserialize)] +pub struct TurnAttemptsSiblingTurnsResponse { + #[serde(default)] + pub sibling_turns: Vec>, +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + fn fixture(name: &str) -> CodeTaskDetailsResponse { + let json = match name { + "diff" => include_str!("../tests/fixtures/task_details_with_diff.json"), + "error" => include_str!("../tests/fixtures/task_details_with_error.json"), + other => panic!("unknown fixture {other}"), + }; + serde_json::from_str(json).expect("fixture should deserialize") + } + + #[test] + fn unified_diff_prefers_current_diff_task_turn() { + let details = fixture("diff"); + let diff = details.unified_diff().expect("diff present"); + assert!(diff.contains("diff --git")); + } + + #[test] + fn unified_diff_falls_back_to_pr_output_diff() { + let details = fixture("error"); + let diff = details.unified_diff().expect("diff from pr output"); + assert!(diff.contains("lib.rs")); + } + + #[test] + fn assistant_text_messages_extracts_text_content() { + let details = fixture("diff"); + let messages = details.assistant_text_messages(); + assert_eq!(messages, vec!["Assistant response".to_string()]); + } + + #[test] + fn user_text_prompt_joins_parts_with_spacing() { + let details = fixture("diff"); + let prompt = details.user_text_prompt().expect("prompt present"); + assert_eq!( + prompt, + "First line + +Second line" + ); + } + + #[test] + fn assistant_error_message_combines_code_and_message() { + let details = fixture("error"); + let msg = details + .assistant_error_message() + .expect("error should be present"); + assert_eq!(msg, "APPLY_FAILED: Patch could not be applied"); + } +} diff --git a/codex-rs/backend-client/tests/fixtures/task_details_with_diff.json b/codex-rs/backend-client/tests/fixtures/task_details_with_diff.json new file mode 100644 index 0000000000..3a06b04c1c --- /dev/null +++ b/codex-rs/backend-client/tests/fixtures/task_details_with_diff.json @@ -0,0 +1,38 @@ +{ + "task": { + "id": "task_123", + "title": "Refactor cloud task client", + "archived": false, + "external_pull_requests": [] + }, + "current_user_turn": { + "input_items": [ + { + "type": "message", + "role": "user", + "content": [ + { "content_type": "text", "text": "First line" }, + { "content_type": "text", "text": "Second line" } + ] + } + ] + }, + "current_assistant_turn": { + "output_items": [ + { + "type": "message", + "content": [ + { "content_type": "text", "text": "Assistant response" } + ] + } + ] + }, + "current_diff_task_turn": { + "output_items": [ + { + "type": "output_diff", + "diff": "diff --git a/src/main.rs b/src/main.rs\n+fn main() { println!(\"hi\"); }\n" + } + ] + } +} diff --git a/codex-rs/backend-client/tests/fixtures/task_details_with_error.json b/codex-rs/backend-client/tests/fixtures/task_details_with_error.json new file mode 100644 index 0000000000..6f6b66a72c --- /dev/null +++ b/codex-rs/backend-client/tests/fixtures/task_details_with_error.json @@ -0,0 +1,22 @@ +{ + "task": { + "id": "task_456", + "title": "Investigate failure", + "archived": false, + "external_pull_requests": [] + }, + "current_assistant_turn": { + "output_items": [ + { + "type": "pr", + "output_diff": { + "diff": "diff --git a/lib.rs b/lib.rs\n+pub fn hello() {}\n" + } + } + ], + "error": { + "code": "APPLY_FAILED", + "message": "Patch could not be applied" + } + } +} diff --git a/codex-rs/chatgpt/Cargo.toml b/codex-rs/chatgpt/Cargo.toml index 97e14d7fe7..0a3170865a 100644 --- a/codex-rs/chatgpt/Cargo.toml +++ b/codex-rs/chatgpt/Cargo.toml @@ -14,6 +14,7 @@ codex-core = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["full"] } +codex-git-apply = { path = "../git-apply" } [dev-dependencies] tempfile = { workspace = true } diff --git a/codex-rs/chatgpt/src/apply_command.rs b/codex-rs/chatgpt/src/apply_command.rs index 52ab205a0c..656197f767 100644 --- a/codex-rs/chatgpt/src/apply_command.rs +++ b/codex-rs/chatgpt/src/apply_command.rs @@ -56,46 +56,24 @@ pub async fn apply_diff_from_task( } async fn apply_diff(diff: &str, cwd: Option) -> anyhow::Result<()> { - let mut cmd = tokio::process::Command::new("git"); - if let Some(cwd) = cwd { - cmd.current_dir(cwd); - } - let toplevel_output = cmd - .args(vec!["rev-parse", "--show-toplevel"]) - .output() - .await?; - - if !toplevel_output.status.success() { - anyhow::bail!("apply must be run from a git repository."); - } - - let repo_root = String::from_utf8(toplevel_output.stdout)? - .trim() - .to_string(); - - let mut git_apply_cmd = tokio::process::Command::new("git") - .args(vec!["apply", "--3way"]) - .current_dir(&repo_root) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .spawn()?; - - if let Some(mut stdin) = git_apply_cmd.stdin.take() { - tokio::io::AsyncWriteExt::write_all(&mut stdin, diff.as_bytes()).await?; - drop(stdin); - } - - let output = git_apply_cmd.wait_with_output().await?; - - if !output.status.success() { + let cwd = cwd.unwrap_or(std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir())); + let req = codex_git_apply::ApplyGitRequest { + cwd, + diff: diff.to_string(), + revert: false, + preflight: false, + }; + let res = codex_git_apply::apply_git_patch(&req)?; + if res.exit_code != 0 { anyhow::bail!( - "Git apply failed with status {}: {}", - output.status, - String::from_utf8_lossy(&output.stderr) + "Git apply failed (applied={}, skipped={}, conflicts={})\nstdout:\n{}\nstderr:\n{}", + res.applied_paths.len(), + res.skipped_paths.len(), + res.conflicted_paths.len(), + res.stdout, + res.stderr ); } - println!("Successfully applied diff"); Ok(()) } diff --git a/codex-rs/chatgpt/src/chatgpt_client.rs b/codex-rs/chatgpt/src/chatgpt_client.rs index a4545f075e..d450f7c59e 100644 --- a/codex-rs/chatgpt/src/chatgpt_client.rs +++ b/codex-rs/chatgpt/src/chatgpt_client.rs @@ -44,6 +44,6 @@ pub(crate) async fn chatgpt_get_request( } else { let status = response.status(); let body = response.text().await.unwrap_or_default(); - anyhow::bail!("Request failed with status {}: {}", status, body) + anyhow::bail!("Request failed with status {status}: {body}") } } diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index c28b90a2b1..c62a267c73 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -29,7 +29,9 @@ codex-mcp-server = { workspace = true } codex-process-hardening = { workspace = true } codex-protocol = { workspace = true } codex-protocol-ts = { workspace = true } +codex-responses-api-proxy = { workspace = true } codex-tui = { workspace = true } +codex-cloud-tasks = { path = "../cloud-tasks" } ctor = { workspace = true } owo-colors = { workspace = true } serde_json = { workspace = true } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index d0d777f568..b99577d092 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -13,8 +13,10 @@ use codex_cli::login::run_login_with_chatgpt; use codex_cli::login::run_login_with_device_code; use codex_cli::login::run_logout; use codex_cli::proto; +use codex_cloud_tasks::Cli as CloudTasksCli; use codex_common::CliConfigOverrides; use codex_exec::Cli as ExecCli; +use codex_responses_api_proxy::Args as ResponsesApiProxyArgs; use codex_tui::AppExitInfo; use codex_tui::Cli as TuiCli; use owo_colors::OwoColorize; @@ -92,6 +94,13 @@ enum Subcommand { /// Internal: generate TypeScript protocol bindings. #[clap(hide = true)] GenerateTs(GenerateTsCommand), + /// [EXPERIMENTAL] Browse tasks from Codex Cloud and apply changes locally. + #[clap(name = "cloud", alias = "cloud-tasks")] + Cloud(CloudTasksCli), + + /// Internal: run the responses API proxy. + #[clap(hide = true)] + ResponsesApiProxy(ResponsesApiProxyArgs), } #[derive(Debug, Parser)] @@ -333,6 +342,13 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() Some(Subcommand::Completion(completion_cli)) => { print_completion(completion_cli); } + Some(Subcommand::Cloud(mut cloud_cli)) => { + prepend_config_flags( + &mut cloud_cli.config_overrides, + root_config_overrides.clone(), + ); + codex_cloud_tasks::run_main(cloud_cli, codex_linux_sandbox_exe).await?; + } Some(Subcommand::Debug(debug_args)) => match debug_args.cmd { DebugCommand::Seatbelt(mut seatbelt_cli) => { prepend_config_flags( @@ -364,6 +380,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() ); run_apply_command(apply_cli, None).await?; } + Some(Subcommand::ResponsesApiProxy(args)) => { + tokio::task::spawn_blocking(move || codex_responses_api_proxy::run_main(args)) + .await??; + } Some(Subcommand::GenerateTs(gen_cli)) => { codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?; } diff --git a/codex-rs/cloud-tasks-client/Cargo.toml b/codex-rs/cloud-tasks-client/Cargo.toml new file mode 100644 index 0000000000..ca45b6e15b --- /dev/null +++ b/codex-rs/cloud-tasks-client/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "codex-cloud-tasks-client" +version = { workspace = true } +edition = "2024" + +[lib] +name = "codex_cloud_tasks_client" +path = "src/lib.rs" + +[lints] +workspace = true + +[features] +default = ["online"] +online = ["dep:codex-backend-client"] +mock = [] + +[dependencies] +anyhow = "1" +async-trait = "0.1" +chrono = { version = "0.4", features = ["serde"] } +diffy = "0.4.2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2.0.12" +codex-backend-client = { path = "../backend-client", optional = true } +codex-git-apply = { path = "../git-apply" } diff --git a/codex-rs/cloud-tasks-client/src/api.rs b/codex-rs/cloud-tasks-client/src/api.rs new file mode 100644 index 0000000000..4bd12939e8 --- /dev/null +++ b/codex-rs/cloud-tasks-client/src/api.rs @@ -0,0 +1,158 @@ +use chrono::DateTime; +use chrono::Utc; +use serde::Deserialize; +use serde::Serialize; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum CloudTaskError { + #[error("unimplemented: {0}")] + Unimplemented(&'static str), + #[error("http error: {0}")] + Http(String), + #[error("io error: {0}")] + Io(String), + #[error("{0}")] + Msg(String), +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct TaskId(pub String); + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum TaskStatus { + Pending, + Ready, + Applied, + Error, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct TaskSummary { + pub id: TaskId, + pub title: String, + pub status: TaskStatus, + pub updated_at: DateTime, + /// Backend environment identifier (when available) + pub environment_id: Option, + /// Human-friendly environment label (when available) + pub environment_label: Option, + pub summary: DiffSummary, + /// True when the backend reports this task as a code review. + #[serde(default)] + pub is_review: bool, + /// Number of assistant attempts (best-of-N), when reported by the backend. + #[serde(default)] + pub attempt_total: Option, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum AttemptStatus { + Pending, + InProgress, + Completed, + Failed, + Cancelled, + #[default] + Unknown, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TurnAttempt { + pub turn_id: String, + pub attempt_placement: Option, + pub created_at: Option>, + pub status: AttemptStatus, + pub diff: Option, + pub messages: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ApplyStatus { + Success, + Partial, + Error, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ApplyOutcome { + pub applied: bool, + pub status: ApplyStatus, + pub message: String, + #[serde(default)] + pub skipped_paths: Vec, + #[serde(default)] + pub conflict_paths: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CreatedTask { + pub id: TaskId, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct DiffSummary { + pub files_changed: usize, + pub lines_added: usize, + pub lines_removed: usize, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TaskText { + pub prompt: Option, + pub messages: Vec, + pub turn_id: Option, + pub sibling_turn_ids: Vec, + pub attempt_placement: Option, + pub attempt_status: AttemptStatus, +} + +impl Default for TaskText { + fn default() -> Self { + Self { + prompt: None, + messages: Vec::new(), + turn_id: None, + sibling_turn_ids: Vec::new(), + attempt_placement: None, + attempt_status: AttemptStatus::Unknown, + } + } +} + +#[async_trait::async_trait] +pub trait CloudBackend: Send + Sync { + async fn list_tasks(&self, env: Option<&str>) -> Result>; + async fn get_task_diff(&self, id: TaskId) -> Result>; + /// Return assistant output messages (no diff) when available. + async fn get_task_messages(&self, id: TaskId) -> Result>; + /// Return the creating prompt and assistant messages (when available). + async fn get_task_text(&self, id: TaskId) -> Result; + /// Return any sibling attempts (best-of-N) for the given assistant turn. + async fn list_sibling_attempts( + &self, + task: TaskId, + turn_id: String, + ) -> Result>; + /// Dry-run apply (preflight) that validates whether the patch would apply cleanly. + /// Never modifies the working tree. When `diff_override` is supplied, the provided diff is + /// used instead of re-fetching the task details so callers can apply alternate attempts. + async fn apply_task_preflight( + &self, + id: TaskId, + diff_override: Option, + ) -> Result; + async fn apply_task(&self, id: TaskId, diff_override: Option) -> Result; + async fn create_task( + &self, + env_id: &str, + prompt: &str, + git_ref: &str, + qa_mode: bool, + best_of_n: usize, + ) -> Result; +} diff --git a/codex-rs/cloud-tasks-client/src/http.rs b/codex-rs/cloud-tasks-client/src/http.rs new file mode 100644 index 0000000000..912681cd3b --- /dev/null +++ b/codex-rs/cloud-tasks-client/src/http.rs @@ -0,0 +1,769 @@ +use crate::ApplyOutcome; +use crate::ApplyStatus; +use crate::AttemptStatus; +use crate::CloudBackend; +use crate::CloudTaskError; +use crate::DiffSummary; +use crate::Result; +use crate::TaskId; +use crate::TaskStatus; +use crate::TaskSummary; +use crate::TurnAttempt; +use crate::api::TaskText; +use chrono::DateTime; +use chrono::Utc; + +use codex_backend_client as backend; +use codex_backend_client::CodeTaskDetailsResponseExt; + +#[derive(Clone)] +pub struct HttpClient { + pub base_url: String, + backend: backend::Client, +} + +impl HttpClient { + pub fn new(base_url: impl Into) -> anyhow::Result { + let base_url = base_url.into(); + let backend = backend::Client::new(base_url.clone())?; + Ok(Self { base_url, backend }) + } + + pub fn with_bearer_token(mut self, token: impl Into) -> Self { + self.backend = self.backend.clone().with_bearer_token(token); + self + } + + pub fn with_user_agent(mut self, ua: impl Into) -> Self { + self.backend = self.backend.clone().with_user_agent(ua); + self + } + + pub fn with_chatgpt_account_id(mut self, account_id: impl Into) -> Self { + self.backend = self.backend.clone().with_chatgpt_account_id(account_id); + self + } + + fn tasks_api(&self) -> api::Tasks<'_> { + api::Tasks::new(self) + } + + fn attempts_api(&self) -> api::Attempts<'_> { + api::Attempts::new(self) + } + + fn apply_api(&self) -> api::Apply<'_> { + api::Apply::new(self) + } +} + +#[async_trait::async_trait] +impl CloudBackend for HttpClient { + async fn list_tasks(&self, env: Option<&str>) -> Result> { + self.tasks_api().list(env).await + } + + async fn get_task_diff(&self, id: TaskId) -> Result> { + self.tasks_api().diff(id).await + } + + async fn get_task_messages(&self, id: TaskId) -> Result> { + self.tasks_api().messages(id).await + } + + async fn get_task_text(&self, id: TaskId) -> Result { + self.tasks_api().task_text(id).await + } + + async fn list_sibling_attempts( + &self, + task: TaskId, + turn_id: String, + ) -> Result> { + self.attempts_api().list(task, turn_id).await + } + + async fn apply_task(&self, id: TaskId, diff_override: Option) -> Result { + self.apply_api().run(id, diff_override, false).await + } + + async fn apply_task_preflight( + &self, + id: TaskId, + diff_override: Option, + ) -> Result { + self.apply_api().run(id, diff_override, true).await + } + + async fn create_task( + &self, + env_id: &str, + prompt: &str, + git_ref: &str, + qa_mode: bool, + best_of_n: usize, + ) -> Result { + self.tasks_api() + .create(env_id, prompt, git_ref, qa_mode, best_of_n) + .await + } +} + +mod api { + use super::*; + use serde_json::Value; + use std::cmp::Ordering; + use std::collections::HashMap; + + pub(crate) struct Tasks<'a> { + base_url: &'a str, + backend: &'a backend::Client, + } + + impl<'a> Tasks<'a> { + pub(crate) fn new(client: &'a HttpClient) -> Self { + Self { + base_url: &client.base_url, + backend: &client.backend, + } + } + + pub(crate) async fn list(&self, env: Option<&str>) -> Result> { + let resp = self + .backend + .list_tasks(Some(20), Some("current"), env) + .await + .map_err(|e| CloudTaskError::Http(format!("list_tasks failed: {e}")))?; + + let tasks: Vec = resp + .items + .into_iter() + .map(map_task_list_item_to_summary) + .collect(); + + append_error_log(&format!( + "http.list_tasks: env={} items={}", + env.unwrap_or(""), + tasks.len() + )); + Ok(tasks) + } + + pub(crate) async fn diff(&self, id: TaskId) -> Result> { + let (details, body, ct) = self + .details_with_body(&id.0) + .await + .map_err(|e| CloudTaskError::Http(format!("get_task_details failed: {e}")))?; + if let Some(diff) = details.unified_diff() { + return Ok(Some(diff)); + } + let _ = (body, ct); + Ok(None) + } + + pub(crate) async fn messages(&self, id: TaskId) -> Result> { + let (details, body, ct) = self + .details_with_body(&id.0) + .await + .map_err(|e| CloudTaskError::Http(format!("get_task_details failed: {e}")))?; + + let mut msgs = details.assistant_text_messages(); + if msgs.is_empty() { + msgs.extend(extract_assistant_messages_from_body(&body)); + } + if !msgs.is_empty() { + return Ok(msgs); + } + if let Some(err) = details.assistant_error_message() { + return Ok(vec![format!("Task failed: {err}")]); + } + + let url = match details_path(self.base_url, &id.0) { + Some(url) => url, + None => format!("{}/api/codex/tasks/{}", self.base_url, id.0), + }; + Err(CloudTaskError::Http(format!( + "No assistant text messages in response. GET {url}; content-type={ct}; body={body}" + ))) + } + + pub(crate) async fn task_text(&self, id: TaskId) -> Result { + let (details, body, _ct) = self + .details_with_body(&id.0) + .await + .map_err(|e| CloudTaskError::Http(format!("get_task_details failed: {e}")))?; + let prompt = details.user_text_prompt(); + let mut messages = details.assistant_text_messages(); + if messages.is_empty() { + messages.extend(extract_assistant_messages_from_body(&body)); + } + let assistant_turn = details.current_assistant_turn.as_ref(); + let turn_id = assistant_turn.and_then(|turn| turn.id.clone()); + let sibling_turn_ids = assistant_turn + .map(|turn| turn.sibling_turn_ids.clone()) + .unwrap_or_default(); + let attempt_placement = assistant_turn.and_then(|turn| turn.attempt_placement); + let attempt_status = attempt_status_from_str( + assistant_turn.and_then(|turn| turn.turn_status.as_deref()), + ); + Ok(TaskText { + prompt, + messages, + turn_id, + sibling_turn_ids, + attempt_placement, + attempt_status, + }) + } + + pub(crate) async fn create( + &self, + env_id: &str, + prompt: &str, + git_ref: &str, + qa_mode: bool, + best_of_n: usize, + ) -> Result { + let mut input_items: Vec = Vec::new(); + input_items.push(serde_json::json!({ + "type": "message", + "role": "user", + "content": [{ "content_type": "text", "text": prompt }] + })); + + if let Ok(diff) = std::env::var("CODEX_STARTING_DIFF") + && !diff.is_empty() + { + input_items.push(serde_json::json!({ + "type": "pre_apply_patch", + "output_diff": { "diff": diff } + })); + } + + let mut request_body = serde_json::json!({ + "new_task": { + "environment_id": env_id, + "branch": git_ref, + "run_environment_in_qa_mode": qa_mode, + }, + "input_items": input_items, + }); + + if best_of_n > 1 + && let Some(obj) = request_body.as_object_mut() + { + obj.insert( + "metadata".to_string(), + serde_json::json!({ "best_of_n": best_of_n }), + ); + } + + match self.backend.create_task(request_body).await { + Ok(id) => { + append_error_log(&format!( + "new_task: created id={id} env={} prompt_chars={}", + env_id, + prompt.chars().count() + )); + Ok(crate::CreatedTask { id: TaskId(id) }) + } + Err(e) => { + append_error_log(&format!( + "new_task: create failed env={} prompt_chars={}: {}", + env_id, + prompt.chars().count(), + e + )); + Err(CloudTaskError::Http(format!("create_task failed: {e}"))) + } + } + } + + async fn details_with_body( + &self, + id: &str, + ) -> anyhow::Result<(backend::CodeTaskDetailsResponse, String, String)> { + let (parsed, body, ct) = self.backend.get_task_details_with_body(id).await?; + Ok((parsed, body, ct)) + } + } + + pub(crate) struct Attempts<'a> { + backend: &'a backend::Client, + } + + impl<'a> Attempts<'a> { + pub(crate) fn new(client: &'a HttpClient) -> Self { + Self { + backend: &client.backend, + } + } + + pub(crate) async fn list(&self, task: TaskId, turn_id: String) -> Result> { + let resp = self + .backend + .list_sibling_turns(&task.0, &turn_id) + .await + .map_err(|e| CloudTaskError::Http(format!("list_sibling_turns failed: {e}")))?; + + let mut attempts: Vec = resp + .sibling_turns + .iter() + .filter_map(turn_attempt_from_map) + .collect(); + attempts.sort_by(compare_attempts); + Ok(attempts) + } + } + + pub(crate) struct Apply<'a> { + backend: &'a backend::Client, + } + + impl<'a> Apply<'a> { + pub(crate) fn new(client: &'a HttpClient) -> Self { + Self { + backend: &client.backend, + } + } + + pub(crate) async fn run( + &self, + task_id: TaskId, + diff_override: Option, + preflight: bool, + ) -> Result { + let id = task_id.0.clone(); + let diff = match diff_override { + Some(diff) => diff, + None => { + let details = self.backend.get_task_details(&id).await.map_err(|e| { + CloudTaskError::Http(format!("get_task_details failed: {e}")) + })?; + details.unified_diff().ok_or_else(|| { + CloudTaskError::Msg(format!("No diff available for task {id}")) + })? + } + }; + + if !is_unified_diff(&diff) { + let summary = summarize_patch_for_logging(&diff); + let mode = if preflight { "preflight" } else { "apply" }; + append_error_log(&format!( + "apply_error: id={id} mode={mode} format=non-unified; {summary}" + )); + return Ok(ApplyOutcome { + applied: false, + status: ApplyStatus::Error, + message: "Expected unified git diff; backend returned an incompatible format." + .to_string(), + skipped_paths: Vec::new(), + conflict_paths: Vec::new(), + }); + } + + let req = codex_git_apply::ApplyGitRequest { + cwd: std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir()), + diff: diff.clone(), + revert: false, + preflight, + }; + let r = codex_git_apply::apply_git_patch(&req) + .map_err(|e| CloudTaskError::Io(format!("git apply failed to run: {e}")))?; + + let status = if r.exit_code == 0 { + ApplyStatus::Success + } else if !r.applied_paths.is_empty() || !r.conflicted_paths.is_empty() { + ApplyStatus::Partial + } else { + ApplyStatus::Error + }; + let applied = matches!(status, ApplyStatus::Success) && !preflight; + + let message = if preflight { + match status { + ApplyStatus::Success => { + format!("Preflight passed for task {id} (applies cleanly)") + } + ApplyStatus::Partial => format!( + "Preflight: patch does not fully apply for task {id} (applied={}, skipped={}, conflicts={})", + r.applied_paths.len(), + r.skipped_paths.len(), + r.conflicted_paths.len() + ), + ApplyStatus::Error => format!( + "Preflight failed for task {id} (applied={}, skipped={}, conflicts={})", + r.applied_paths.len(), + r.skipped_paths.len(), + r.conflicted_paths.len() + ), + } + } else { + match status { + ApplyStatus::Success => format!( + "Applied task {id} locally ({} files)", + r.applied_paths.len() + ), + ApplyStatus::Partial => format!( + "Apply partially succeeded for task {id} (applied={}, skipped={}, conflicts={})", + r.applied_paths.len(), + r.skipped_paths.len(), + r.conflicted_paths.len() + ), + ApplyStatus::Error => format!( + "Apply failed for task {id} (applied={}, skipped={}, conflicts={})", + r.applied_paths.len(), + r.skipped_paths.len(), + r.conflicted_paths.len() + ), + } + }; + + if matches!(status, ApplyStatus::Partial | ApplyStatus::Error) + || (preflight && !matches!(status, ApplyStatus::Success)) + { + let mut log = String::new(); + let summary = summarize_patch_for_logging(&diff); + let mode = if preflight { "preflight" } else { "apply" }; + use std::fmt::Write as _; + let _ = writeln!( + &mut log, + "apply_result: mode={} id={} status={:?} applied={} skipped={} conflicts={} cmd={}", + mode, + id, + status, + r.applied_paths.len(), + r.skipped_paths.len(), + r.conflicted_paths.len(), + r.cmd_for_log + ); + let _ = writeln!( + &mut log, + "stdout_tail=\n{}\nstderr_tail=\n{}", + tail(&r.stdout, 2000), + tail(&r.stderr, 2000) + ); + let _ = writeln!(&mut log, "{summary}"); + let _ = writeln!( + &mut log, + "----- PATCH BEGIN -----\n{diff}\n----- PATCH END -----" + ); + append_error_log(&log); + } + + Ok(ApplyOutcome { + applied, + status, + message, + skipped_paths: r.skipped_paths, + conflict_paths: r.conflicted_paths, + }) + } + } + + fn details_path(base_url: &str, id: &str) -> Option { + if base_url.contains("/backend-api") { + Some(format!("{base_url}/wham/tasks/{id}")) + } else if base_url.contains("/api/codex") { + Some(format!("{base_url}/tasks/{id}")) + } else { + None + } + } + + fn extract_assistant_messages_from_body(body: &str) -> Vec { + let mut msgs = Vec::new(); + if let Ok(full) = serde_json::from_str::(body) + && let Some(arr) = full + .get("current_assistant_turn") + .and_then(|v| v.get("worklog")) + .and_then(|v| v.get("messages")) + .and_then(|v| v.as_array()) + { + for m in arr { + let is_assistant = m + .get("author") + .and_then(|a| a.get("role")) + .and_then(|r| r.as_str()) + == Some("assistant"); + if !is_assistant { + continue; + } + if let Some(parts) = m + .get("content") + .and_then(|c| c.get("parts")) + .and_then(|p| p.as_array()) + { + for p in parts { + if let Some(s) = p.as_str() { + if !s.is_empty() { + msgs.push(s.to_string()); + } + continue; + } + if let Some(obj) = p.as_object() + && obj.get("content_type").and_then(|t| t.as_str()) == Some("text") + && let Some(txt) = obj.get("text").and_then(|t| t.as_str()) + { + msgs.push(txt.to_string()); + } + } + } + } + } + msgs + } + + fn turn_attempt_from_map(turn: &HashMap) -> Option { + let turn_id = turn.get("id").and_then(Value::as_str)?.to_string(); + let attempt_placement = turn.get("attempt_placement").and_then(Value::as_i64); + let created_at = parse_timestamp_value(turn.get("created_at")); + let status = attempt_status_from_str(turn.get("turn_status").and_then(Value::as_str)); + let diff = extract_diff_from_turn(turn); + let messages = extract_assistant_messages_from_turn(turn); + Some(TurnAttempt { + turn_id, + attempt_placement, + created_at, + status, + diff, + messages, + }) + } + + fn compare_attempts(a: &TurnAttempt, b: &TurnAttempt) -> Ordering { + match (a.attempt_placement, b.attempt_placement) { + (Some(lhs), Some(rhs)) => lhs.cmp(&rhs), + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => match (a.created_at, b.created_at) { + (Some(lhs), Some(rhs)) => lhs.cmp(&rhs), + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => a.turn_id.cmp(&b.turn_id), + }, + } + } + + fn extract_diff_from_turn(turn: &HashMap) -> Option { + let items = turn.get("output_items").and_then(Value::as_array)?; + for item in items { + match item.get("type").and_then(Value::as_str) { + Some("output_diff") => { + if let Some(diff) = item.get("diff").and_then(Value::as_str) + && !diff.is_empty() + { + return Some(diff.to_string()); + } + } + Some("pr") => { + if let Some(diff) = item + .get("output_diff") + .and_then(Value::as_object) + .and_then(|od| od.get("diff")) + .and_then(Value::as_str) + && !diff.is_empty() + { + return Some(diff.to_string()); + } + } + _ => {} + } + } + None + } + + fn extract_assistant_messages_from_turn(turn: &HashMap) -> Vec { + let mut msgs = Vec::new(); + if let Some(items) = turn.get("output_items").and_then(Value::as_array) { + for item in items { + if item.get("type").and_then(Value::as_str) != Some("message") { + continue; + } + if let Some(content) = item.get("content").and_then(Value::as_array) { + for part in content { + if part.get("content_type").and_then(Value::as_str) == Some("text") + && let Some(txt) = part.get("text").and_then(Value::as_str) + && !txt.is_empty() + { + msgs.push(txt.to_string()); + } + } + } + } + } + msgs + } + + fn attempt_status_from_str(raw: Option<&str>) -> AttemptStatus { + match raw.unwrap_or_default() { + "failed" => AttemptStatus::Failed, + "completed" => AttemptStatus::Completed, + "in_progress" => AttemptStatus::InProgress, + "pending" => AttemptStatus::Pending, + _ => AttemptStatus::Pending, + } + } + + fn parse_timestamp_value(v: Option<&Value>) -> Option> { + let ts = v?.as_f64()?; + let secs = ts as i64; + let nanos = ((ts - secs as f64) * 1_000_000_000.0) as u32; + Some(DateTime::::from( + std::time::UNIX_EPOCH + std::time::Duration::new(secs.max(0) as u64, nanos), + )) + } + + fn map_task_list_item_to_summary(src: backend::TaskListItem) -> TaskSummary { + let status_display = src.task_status_display.as_ref(); + TaskSummary { + id: TaskId(src.id), + title: src.title, + status: map_status(status_display), + updated_at: parse_updated_at(src.updated_at.as_ref()), + environment_id: None, + environment_label: env_label_from_status_display(status_display), + summary: diff_summary_from_status_display(status_display), + is_review: src + .pull_requests + .as_ref() + .is_some_and(|prs| !prs.is_empty()), + attempt_total: attempt_total_from_status_display(status_display), + } + } + + fn map_status(v: Option<&HashMap>) -> TaskStatus { + if let Some(val) = v { + if let Some(turn) = val + .get("latest_turn_status_display") + .and_then(Value::as_object) + && let Some(s) = turn.get("turn_status").and_then(Value::as_str) + { + return match s { + "failed" => TaskStatus::Error, + "completed" => TaskStatus::Ready, + "in_progress" => TaskStatus::Pending, + "pending" => TaskStatus::Pending, + "cancelled" => TaskStatus::Error, + _ => TaskStatus::Pending, + }; + } + if let Some(state) = val.get("state").and_then(Value::as_str) { + return match state { + "pending" => TaskStatus::Pending, + "ready" => TaskStatus::Ready, + "applied" => TaskStatus::Applied, + "error" => TaskStatus::Error, + _ => TaskStatus::Pending, + }; + } + } + TaskStatus::Pending + } + + fn parse_updated_at(ts: Option<&f64>) -> DateTime { + if let Some(v) = ts { + let secs = *v as i64; + let nanos = ((*v - secs as f64) * 1_000_000_000.0) as u32; + return DateTime::::from( + std::time::UNIX_EPOCH + std::time::Duration::new(secs.max(0) as u64, nanos), + ); + } + Utc::now() + } + + fn env_label_from_status_display(v: Option<&HashMap>) -> Option { + let map = v?; + map.get("environment_label") + .and_then(Value::as_str) + .map(str::to_string) + } + + fn diff_summary_from_status_display(v: Option<&HashMap>) -> DiffSummary { + let mut out = DiffSummary::default(); + let Some(map) = v else { return out }; + let latest = map + .get("latest_turn_status_display") + .and_then(Value::as_object); + let Some(latest) = latest else { return out }; + if let Some(ds) = latest.get("diff_stats").and_then(Value::as_object) { + if let Some(n) = ds.get("files_modified").and_then(Value::as_i64) { + out.files_changed = n.max(0) as usize; + } + if let Some(n) = ds.get("lines_added").and_then(Value::as_i64) { + out.lines_added = n.max(0) as usize; + } + if let Some(n) = ds.get("lines_removed").and_then(Value::as_i64) { + out.lines_removed = n.max(0) as usize; + } + } + out + } + + fn attempt_total_from_status_display(v: Option<&HashMap>) -> Option { + let map = v?; + let latest = map + .get("latest_turn_status_display") + .and_then(Value::as_object)?; + let siblings = latest.get("sibling_turn_ids").and_then(Value::as_array)?; + Some(siblings.len().saturating_add(1)) + } + + fn is_unified_diff(diff: &str) -> bool { + let t = diff.trim_start(); + if t.starts_with("diff --git ") { + return true; + } + let has_dash_headers = diff.contains("\n--- ") && diff.contains("\n+++ "); + let has_hunk = diff.contains("\n@@ ") || diff.starts_with("@@ "); + has_dash_headers && has_hunk + } + + fn tail(s: &str, max: usize) -> String { + if s.len() <= max { + s.to_string() + } else { + s[s.len() - max..].to_string() + } + } + + fn summarize_patch_for_logging(patch: &str) -> String { + let trimmed = patch.trim_start(); + let kind = if trimmed.starts_with("*** Begin Patch") { + "codex-patch" + } else if trimmed.starts_with("diff --git ") || trimmed.contains("\n*** End Patch\n") { + "git-diff" + } else if trimmed.starts_with("@@ ") || trimmed.contains("\n@@ ") { + "unified-diff" + } else { + "unknown" + }; + let lines = patch.lines().count(); + let chars = patch.len(); + let cwd = std::env::current_dir() + .ok() + .map(|p| p.display().to_string()) + .unwrap_or_else(|| "".to_string()); + let head: String = patch.lines().take(20).collect::>().join("\n"); + let head_trunc = if head.len() > 800 { + format!("{}…", &head[..800]) + } else { + head + }; + format!( + "patch_summary: kind={kind} lines={lines} chars={chars} cwd={cwd} ; head=\n{head_trunc}" + ) + } +} + +fn append_error_log(message: &str) { + let ts = Utc::now().to_rfc3339(); + if let Ok(mut f) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open("error.log") + { + use std::io::Write as _; + let _ = writeln!(f, "[{ts}] {message}"); + } +} diff --git a/codex-rs/cloud-tasks-client/src/lib.rs b/codex-rs/cloud-tasks-client/src/lib.rs new file mode 100644 index 0000000000..1c5a01e2aa --- /dev/null +++ b/codex-rs/cloud-tasks-client/src/lib.rs @@ -0,0 +1,29 @@ +mod api; + +pub use api::ApplyOutcome; +pub use api::ApplyStatus; +pub use api::AttemptStatus; +pub use api::CloudBackend; +pub use api::CloudTaskError; +pub use api::CreatedTask; +pub use api::DiffSummary; +pub use api::Result; +pub use api::TaskId; +pub use api::TaskStatus; +pub use api::TaskSummary; +pub use api::TaskText; +pub use api::TurnAttempt; + +#[cfg(feature = "mock")] +mod mock; + +#[cfg(feature = "online")] +mod http; + +#[cfg(feature = "mock")] +pub use mock::MockClient; + +#[cfg(feature = "online")] +pub use http::HttpClient; + +// Reusable apply engine now lives in the shared crate `codex-git-apply`. diff --git a/codex-rs/cloud-tasks-client/src/mock.rs b/codex-rs/cloud-tasks-client/src/mock.rs new file mode 100644 index 0000000000..97bc5520a8 --- /dev/null +++ b/codex-rs/cloud-tasks-client/src/mock.rs @@ -0,0 +1,180 @@ +use crate::ApplyOutcome; +use crate::AttemptStatus; +use crate::CloudBackend; +use crate::DiffSummary; +use crate::Result; +use crate::TaskId; +use crate::TaskStatus; +use crate::TaskSummary; +use crate::TurnAttempt; +use crate::api::TaskText; +use chrono::Utc; + +#[derive(Clone, Default)] +pub struct MockClient; + +#[async_trait::async_trait] +impl CloudBackend for MockClient { + async fn list_tasks(&self, _env: Option<&str>) -> Result> { + // Slightly vary content by env to aid tests that rely on the mock + let rows = match _env { + Some("env-A") => vec![("T-2000", "A: First", TaskStatus::Ready)], + Some("env-B") => vec![ + ("T-3000", "B: One", TaskStatus::Ready), + ("T-3001", "B: Two", TaskStatus::Pending), + ], + _ => vec![ + ("T-1000", "Update README formatting", TaskStatus::Ready), + ("T-1001", "Fix clippy warnings in core", TaskStatus::Pending), + ("T-1002", "Add contributing guide", TaskStatus::Ready), + ], + }; + let environment_id = _env.map(str::to_string); + let environment_label = match _env { + Some("env-A") => Some("Env A".to_string()), + Some("env-B") => Some("Env B".to_string()), + Some(other) => Some(other.to_string()), + None => Some("Global".to_string()), + }; + let mut out = Vec::new(); + for (id_str, title, status) in rows { + let id = TaskId(id_str.to_string()); + let diff = mock_diff_for(&id); + let (a, d) = count_from_unified(&diff); + out.push(TaskSummary { + id, + title: title.to_string(), + status, + updated_at: Utc::now(), + environment_id: environment_id.clone(), + environment_label: environment_label.clone(), + summary: DiffSummary { + files_changed: 1, + lines_added: a, + lines_removed: d, + }, + is_review: false, + attempt_total: Some(if id_str == "T-1000" { 2 } else { 1 }), + }); + } + Ok(out) + } + + async fn get_task_diff(&self, id: TaskId) -> Result> { + Ok(Some(mock_diff_for(&id))) + } + + async fn get_task_messages(&self, _id: TaskId) -> Result> { + Ok(vec![ + "Mock assistant output: this task contains no diff.".to_string(), + ]) + } + + async fn get_task_text(&self, _id: TaskId) -> Result { + Ok(TaskText { + prompt: Some("Why is there no diff?".to_string()), + messages: vec!["Mock assistant output: this task contains no diff.".to_string()], + turn_id: Some("mock-turn".to_string()), + sibling_turn_ids: Vec::new(), + attempt_placement: Some(0), + attempt_status: AttemptStatus::Completed, + }) + } + + async fn apply_task(&self, id: TaskId, _diff_override: Option) -> Result { + Ok(ApplyOutcome { + applied: true, + status: crate::ApplyStatus::Success, + message: format!("Applied task {} locally (mock)", id.0), + skipped_paths: Vec::new(), + conflict_paths: Vec::new(), + }) + } + + async fn apply_task_preflight( + &self, + id: TaskId, + _diff_override: Option, + ) -> Result { + Ok(ApplyOutcome { + applied: false, + status: crate::ApplyStatus::Success, + message: format!("Preflight passed for task {} (mock)", id.0), + skipped_paths: Vec::new(), + conflict_paths: Vec::new(), + }) + } + + async fn list_sibling_attempts( + &self, + task: TaskId, + _turn_id: String, + ) -> Result> { + if task.0 == "T-1000" { + return Ok(vec![TurnAttempt { + turn_id: "T-1000-attempt-2".to_string(), + attempt_placement: Some(1), + created_at: Some(Utc::now()), + status: AttemptStatus::Completed, + diff: Some(mock_diff_for(&task)), + messages: vec!["Mock alternate attempt".to_string()], + }]); + } + Ok(Vec::new()) + } + + async fn create_task( + &self, + env_id: &str, + prompt: &str, + git_ref: &str, + qa_mode: bool, + best_of_n: usize, + ) -> Result { + let _ = (env_id, prompt, git_ref, qa_mode, best_of_n); + let id = format!("task_local_{}", chrono::Utc::now().timestamp_millis()); + Ok(crate::CreatedTask { id: TaskId(id) }) + } +} + +fn mock_diff_for(id: &TaskId) -> String { + match id.0.as_str() { + "T-1000" => { + "diff --git a/README.md b/README.md\nindex 000000..111111 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,2 +1,3 @@\n Intro\n-Hello\n+Hello, world!\n+Task: T-1000\n".to_string() + } + "T-1001" => { + "diff --git a/core/src/lib.rs b/core/src/lib.rs\nindex 000000..111111 100644\n--- a/core/src/lib.rs\n+++ b/core/src/lib.rs\n@@ -1,2 +1,1 @@\n-use foo;\n use bar;\n".to_string() + } + _ => { + "diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md\nindex 000000..111111 100644\n--- /dev/null\n+++ b/CONTRIBUTING.md\n@@ -0,0 +1,3 @@\n+## Contributing\n+Please open PRs.\n+Thanks!\n".to_string() + } + } +} + +fn count_from_unified(diff: &str) -> (usize, usize) { + if let Ok(patch) = diffy::Patch::from_str(diff) { + patch + .hunks() + .iter() + .flat_map(diffy::Hunk::lines) + .fold((0, 0), |(a, d), l| match l { + diffy::Line::Insert(_) => (a + 1, d), + diffy::Line::Delete(_) => (a, d + 1), + _ => (a, d), + }) + } else { + let mut a = 0; + let mut d = 0; + for l in diff.lines() { + if l.starts_with("+++") || l.starts_with("---") || l.starts_with("@@") { + continue; + } + match l.as_bytes().first() { + Some(b'+') => a += 1, + Some(b'-') => d += 1, + _ => {} + } + } + (a, d) + } +} diff --git a/codex-rs/cloud-tasks/Cargo.toml b/codex-rs/cloud-tasks/Cargo.toml new file mode 100644 index 0000000000..d0cee3ff1a --- /dev/null +++ b/codex-rs/cloud-tasks/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "codex-cloud-tasks" +version = { workspace = true } +edition = "2024" + +[lib] +name = "codex_cloud_tasks" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +codex-common = { path = "../common", features = ["cli"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +tracing = { version = "0.1.41", features = ["log"] } +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +codex-cloud-tasks-client = { path = "../cloud-tasks-client", features = ["mock", "online"] } +ratatui = { version = "0.29.0" } +crossterm = { version = "0.28.1", features = ["event-stream"] } +tokio-stream = "0.1.17" +chrono = { version = "0.4", features = ["serde"] } +codex-login = { path = "../login" } +codex-core = { path = "../core" } +throbber-widgets-tui = "0.8.0" +base64 = "0.22" +serde_json = "1" +reqwest = { version = "0.12", features = ["json"] } +serde = { version = "1", features = ["derive"] } +unicode-width = "0.1" +codex-tui = { path = "../tui" } + +[dev-dependencies] +async-trait = "0.1" diff --git a/codex-rs/cloud-tasks/src/app.rs b/codex-rs/cloud-tasks/src/app.rs new file mode 100644 index 0000000000..adf17419ee --- /dev/null +++ b/codex-rs/cloud-tasks/src/app.rs @@ -0,0 +1,482 @@ +use std::time::Duration; + +// Environment filter data models for the TUI +#[derive(Clone, Debug, Default)] +pub struct EnvironmentRow { + pub id: String, + pub label: Option, + pub is_pinned: bool, + pub repo_hints: Option, // e.g., "openai/codex" +} + +#[derive(Clone, Debug, Default)] +pub struct EnvModalState { + pub query: String, + pub selected: usize, +} + +#[derive(Clone, Debug, Default)] +pub struct BestOfModalState { + pub selected: usize, +} + +#[derive(Clone, Debug, Copy, PartialEq, Eq)] +pub enum ApplyResultLevel { + Success, + Partial, + Error, +} + +#[derive(Clone, Debug)] +pub struct ApplyModalState { + pub task_id: TaskId, + pub title: String, + pub result_message: Option, + pub result_level: Option, + pub skipped_paths: Vec, + pub conflict_paths: Vec, + pub diff_override: Option, +} + +use crate::scrollable_diff::ScrollableDiff; +use codex_cloud_tasks_client::CloudBackend; +use codex_cloud_tasks_client::TaskId; +use codex_cloud_tasks_client::TaskSummary; +use throbber_widgets_tui::ThrobberState; + +#[derive(Default)] +pub struct App { + pub tasks: Vec, + pub selected: usize, + pub status: String, + pub diff_overlay: Option, + pub throbber: ThrobberState, + pub refresh_inflight: bool, + pub details_inflight: bool, + // Environment filter state + pub env_filter: Option, + pub env_modal: Option, + pub apply_modal: Option, + pub best_of_modal: Option, + pub environments: Vec, + pub env_last_loaded: Option, + pub env_loading: bool, + pub env_error: Option, + // New Task page + pub new_task: Option, + pub best_of_n: usize, + // Apply preflight spinner state + pub apply_preflight_inflight: bool, + // Apply action spinner state + pub apply_inflight: bool, + // Background enrichment coordination + pub list_generation: u64, + pub in_flight: std::collections::HashSet, + // Background enrichment caches were planned; currently unused. +} + +impl App { + pub fn new() -> Self { + Self { + tasks: Vec::new(), + selected: 0, + status: "Press r to refresh".to_string(), + diff_overlay: None, + throbber: ThrobberState::default(), + refresh_inflight: false, + details_inflight: false, + env_filter: None, + env_modal: None, + apply_modal: None, + best_of_modal: None, + environments: Vec::new(), + env_last_loaded: None, + env_loading: false, + env_error: None, + new_task: None, + best_of_n: 1, + apply_preflight_inflight: false, + apply_inflight: false, + list_generation: 0, + in_flight: std::collections::HashSet::new(), + } + } + + pub fn next(&mut self) { + if self.tasks.is_empty() { + return; + } + self.selected = (self.selected + 1).min(self.tasks.len().saturating_sub(1)); + } + + pub fn prev(&mut self) { + if self.tasks.is_empty() { + return; + } + if self.selected > 0 { + self.selected -= 1; + } + } +} + +pub async fn load_tasks( + backend: &dyn CloudBackend, + env: Option<&str>, +) -> anyhow::Result> { + // In later milestones, add a small debounce, spinner, and error display. + let tasks = tokio::time::timeout(Duration::from_secs(5), backend.list_tasks(env)).await??; + // Hide review-only tasks from the main list. + let filtered: Vec = tasks.into_iter().filter(|t| !t.is_review).collect(); + Ok(filtered) +} + +pub struct DiffOverlay { + pub title: String, + pub task_id: TaskId, + pub sd: ScrollableDiff, + pub base_can_apply: bool, + pub diff_lines: Vec, + pub text_lines: Vec, + pub prompt: Option, + pub attempts: Vec, + pub selected_attempt: usize, + pub current_view: DetailView, + pub base_turn_id: Option, + pub sibling_turn_ids: Vec, + pub attempt_total_hint: Option, +} + +#[derive(Clone, Debug, Default)] +pub struct AttemptView { + pub turn_id: Option, + pub status: codex_cloud_tasks_client::AttemptStatus, + pub attempt_placement: Option, + pub diff_lines: Vec, + pub text_lines: Vec, + pub prompt: Option, + pub diff_raw: Option, +} + +impl AttemptView { + pub fn has_diff(&self) -> bool { + !self.diff_lines.is_empty() + } + + pub fn has_text(&self) -> bool { + !self.text_lines.is_empty() || self.prompt.is_some() + } +} + +impl DiffOverlay { + pub fn new(task_id: TaskId, title: String, attempt_total_hint: Option) -> Self { + let mut sd = ScrollableDiff::new(); + sd.set_content(Vec::new()); + Self { + title, + task_id, + sd, + base_can_apply: false, + diff_lines: Vec::new(), + text_lines: Vec::new(), + prompt: None, + attempts: vec![AttemptView::default()], + selected_attempt: 0, + current_view: DetailView::Prompt, + base_turn_id: None, + sibling_turn_ids: Vec::new(), + attempt_total_hint, + } + } + + pub fn current_attempt(&self) -> Option<&AttemptView> { + self.attempts.get(self.selected_attempt) + } + + pub fn base_attempt_mut(&mut self) -> &mut AttemptView { + if self.attempts.is_empty() { + self.attempts.push(AttemptView::default()); + } + &mut self.attempts[0] + } + + pub fn set_view(&mut self, view: DetailView) { + self.current_view = view; + self.apply_selection_to_fields(); + } + + pub fn expected_attempts(&self) -> Option { + self.attempt_total_hint.or({ + if self.attempts.is_empty() { + None + } else { + Some(self.attempts.len()) + } + }) + } + + pub fn attempt_count(&self) -> usize { + self.attempts.len() + } + + pub fn attempt_display_total(&self) -> usize { + self.expected_attempts() + .unwrap_or_else(|| self.attempts.len().max(1)) + } + + pub fn step_attempt(&mut self, delta: isize) -> bool { + let total = self.attempts.len(); + if total <= 1 { + return false; + } + let total_isize = total as isize; + let current = self.selected_attempt as isize; + let mut next = current + delta; + next = ((next % total_isize) + total_isize) % total_isize; + let next = next as usize; + self.selected_attempt = next; + self.apply_selection_to_fields(); + true + } + + pub fn current_can_apply(&self) -> bool { + matches!(self.current_view, DetailView::Diff) + && self + .current_attempt() + .and_then(|attempt| attempt.diff_raw.as_ref()) + .map(|diff| !diff.is_empty()) + .unwrap_or(false) + } + + pub fn apply_selection_to_fields(&mut self) { + let (diff_lines, text_lines, prompt) = if let Some(attempt) = self.current_attempt() { + ( + attempt.diff_lines.clone(), + attempt.text_lines.clone(), + attempt.prompt.clone(), + ) + } else { + self.diff_lines.clear(); + self.text_lines.clear(); + self.prompt = None; + self.sd.set_content(vec!["".to_string()]); + return; + }; + + self.diff_lines = diff_lines.clone(); + self.text_lines = text_lines.clone(); + self.prompt = prompt; + + match self.current_view { + DetailView::Diff => { + if diff_lines.is_empty() { + self.sd.set_content(vec!["".to_string()]); + } else { + self.sd.set_content(diff_lines); + } + } + DetailView::Prompt => { + if text_lines.is_empty() { + self.sd.set_content(vec!["".to_string()]); + } else { + self.sd.set_content(text_lines); + } + } + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DetailView { + Diff, + Prompt, +} + +/// Internal app events delivered from background tasks. +/// These let the UI event loop remain responsive and keep the spinner animating. +#[derive(Debug)] +pub enum AppEvent { + TasksLoaded { + env: Option, + result: anyhow::Result>, + }, + // Background diff summary events were planned; removed for now to keep code minimal. + /// Autodetection of a likely environment id finished + EnvironmentAutodetected(anyhow::Result), + /// Background completion of environment list fetch + EnvironmentsLoaded(anyhow::Result>), + DetailsDiffLoaded { + id: TaskId, + title: String, + diff: String, + }, + DetailsMessagesLoaded { + id: TaskId, + title: String, + messages: Vec, + prompt: Option, + turn_id: Option, + sibling_turn_ids: Vec, + attempt_placement: Option, + attempt_status: codex_cloud_tasks_client::AttemptStatus, + }, + DetailsFailed { + id: TaskId, + title: String, + error: String, + }, + AttemptsLoaded { + id: TaskId, + attempts: Vec, + }, + /// Background completion of new task submission + NewTaskSubmitted(Result), + /// Background completion of apply preflight when opening modal or on demand + ApplyPreflightFinished { + id: TaskId, + title: String, + message: String, + level: ApplyResultLevel, + skipped: Vec, + conflicts: Vec, + }, + /// Background completion of apply action (actual patch application) + ApplyFinished { + id: TaskId, + result: std::result::Result, + }, +} + +// Convenience aliases; currently unused. +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + + struct FakeBackend { + // maps env key to titles + by_env: std::collections::HashMap, Vec<&'static str>>, + } + + #[async_trait::async_trait] + impl codex_cloud_tasks_client::CloudBackend for FakeBackend { + async fn list_tasks( + &self, + env: Option<&str>, + ) -> codex_cloud_tasks_client::Result> { + let key = env.map(str::to_string); + let titles = self + .by_env + .get(&key) + .cloned() + .unwrap_or_else(|| vec!["default-a", "default-b"]); + let mut out = Vec::new(); + for (i, t) in titles.into_iter().enumerate() { + out.push(TaskSummary { + id: TaskId(format!("T-{i}")), + title: t.to_string(), + status: codex_cloud_tasks_client::TaskStatus::Ready, + updated_at: Utc::now(), + environment_id: env.map(str::to_string), + environment_label: None, + summary: codex_cloud_tasks_client::DiffSummary::default(), + is_review: false, + attempt_total: Some(1), + }); + } + Ok(out) + } + + async fn get_task_diff( + &self, + _id: TaskId, + ) -> codex_cloud_tasks_client::Result> { + Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented( + "not used in test", + )) + } + + async fn get_task_messages( + &self, + _id: TaskId, + ) -> codex_cloud_tasks_client::Result> { + Ok(vec![]) + } + async fn get_task_text( + &self, + _id: TaskId, + ) -> codex_cloud_tasks_client::Result { + Ok(codex_cloud_tasks_client::TaskText { + prompt: Some("Example prompt".to_string()), + messages: Vec::new(), + turn_id: Some("fake-turn".to_string()), + sibling_turn_ids: Vec::new(), + attempt_placement: Some(0), + attempt_status: codex_cloud_tasks_client::AttemptStatus::Completed, + }) + } + + async fn list_sibling_attempts( + &self, + _task: TaskId, + _turn_id: String, + ) -> codex_cloud_tasks_client::Result> { + Ok(Vec::new()) + } + + async fn apply_task( + &self, + _id: TaskId, + _diff_override: Option, + ) -> codex_cloud_tasks_client::Result { + Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented( + "not used in test", + )) + } + + async fn apply_task_preflight( + &self, + _id: TaskId, + _diff_override: Option, + ) -> codex_cloud_tasks_client::Result { + Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented( + "not used in test", + )) + } + + async fn create_task( + &self, + _env_id: &str, + _prompt: &str, + _git_ref: &str, + _qa_mode: bool, + _best_of_n: usize, + ) -> codex_cloud_tasks_client::Result { + Err(codex_cloud_tasks_client::CloudTaskError::Unimplemented( + "not used in test", + )) + } + } + + #[tokio::test] + async fn load_tasks_uses_env_parameter() { + // Arrange: env-specific task titles + let mut by_env = std::collections::HashMap::new(); + by_env.insert(None, vec!["root-1", "root-2"]); + by_env.insert(Some("env-A".to_string()), vec!["A-1"]); + by_env.insert(Some("env-B".to_string()), vec!["B-1", "B-2", "B-3"]); + let backend = FakeBackend { by_env }; + + // Act + Assert + let root = load_tasks(&backend, None).await.unwrap(); + assert_eq!(root.len(), 2); + assert_eq!(root[0].title, "root-1"); + + let a = load_tasks(&backend, Some("env-A")).await.unwrap(); + assert_eq!(a.len(), 1); + assert_eq!(a[0].title, "A-1"); + + let b = load_tasks(&backend, Some("env-B")).await.unwrap(); + assert_eq!(b.len(), 3); + assert_eq!(b[2].title, "B-3"); + } +} diff --git a/codex-rs/cloud-tasks/src/cli.rs b/codex-rs/cloud-tasks/src/cli.rs new file mode 100644 index 0000000000..81125aeb1c --- /dev/null +++ b/codex-rs/cloud-tasks/src/cli.rs @@ -0,0 +1,9 @@ +use clap::Parser; +use codex_common::CliConfigOverrides; + +#[derive(Parser, Debug, Default)] +#[command(version)] +pub struct Cli { + #[clap(skip)] + pub config_overrides: CliConfigOverrides, +} diff --git a/codex-rs/cloud-tasks/src/env_detect.rs b/codex-rs/cloud-tasks/src/env_detect.rs new file mode 100644 index 0000000000..e7e8fb6b16 --- /dev/null +++ b/codex-rs/cloud-tasks/src/env_detect.rs @@ -0,0 +1,361 @@ +use reqwest::header::CONTENT_TYPE; +use reqwest::header::HeaderMap; +use std::collections::HashMap; +use tracing::info; +use tracing::warn; + +#[derive(Debug, Clone, serde::Deserialize)] +struct CodeEnvironment { + id: String, + #[serde(default)] + label: Option, + #[serde(default)] + is_pinned: Option, + #[serde(default)] + task_count: Option, +} + +#[derive(Debug, Clone)] +pub struct AutodetectSelection { + pub id: String, + pub label: Option, +} + +pub async fn autodetect_environment_id( + base_url: &str, + headers: &HeaderMap, + desired_label: Option, +) -> anyhow::Result { + // 1) Try repo-specific environments based on local git origins (GitHub only, like VSCode) + let origins = get_git_origins(); + crate::append_error_log(format!("env: git origins: {origins:?}")); + let mut by_repo_envs: Vec = Vec::new(); + for origin in &origins { + if let Some((owner, repo)) = parse_owner_repo(origin) { + let url = if base_url.contains("/backend-api") { + format!( + "{}/wham/environments/by-repo/{}/{}/{}", + base_url, "github", owner, repo + ) + } else { + format!( + "{}/api/codex/environments/by-repo/{}/{}/{}", + base_url, "github", owner, repo + ) + }; + crate::append_error_log(format!("env: GET {url}")); + match get_json::>(&url, headers).await { + Ok(mut list) => { + crate::append_error_log(format!( + "env: by-repo returned {} env(s) for {owner}/{repo}", + list.len(), + )); + by_repo_envs.append(&mut list); + } + Err(e) => crate::append_error_log(format!( + "env: by-repo fetch failed for {owner}/{repo}: {e}" + )), + } + } + } + if let Some(env) = pick_environment_row(&by_repo_envs, desired_label.as_deref()) { + return Ok(AutodetectSelection { + id: env.id.clone(), + label: env.label.as_deref().map(str::to_owned), + }); + } + + // 2) Fallback to the full list + let list_url = if base_url.contains("/backend-api") { + format!("{base_url}/wham/environments") + } else { + format!("{base_url}/api/codex/environments") + }; + crate::append_error_log(format!("env: GET {list_url}")); + // Fetch and log the full environments JSON for debugging + let http = reqwest::Client::builder().build()?; + let res = http.get(&list_url).headers(headers.clone()).send().await?; + let status = res.status(); + let ct = res + .headers() + .get(CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + let body = res.text().await.unwrap_or_default(); + crate::append_error_log(format!("env: status={status} content-type={ct}")); + match serde_json::from_str::(&body) { + Ok(v) => { + let pretty = serde_json::to_string_pretty(&v).unwrap_or(body.clone()); + crate::append_error_log(format!("env: /environments JSON (pretty):\n{pretty}")); + } + Err(_) => crate::append_error_log(format!("env: /environments (raw):\n{body}")), + } + if !status.is_success() { + anyhow::bail!("GET {list_url} failed: {status}; content-type={ct}; body={body}"); + } + let all_envs: Vec = serde_json::from_str(&body).map_err(|e| { + anyhow::anyhow!("Decode error for {list_url}: {e}; content-type={ct}; body={body}") + })?; + if let Some(env) = pick_environment_row(&all_envs, desired_label.as_deref()) { + return Ok(AutodetectSelection { + id: env.id.clone(), + label: env.label.as_deref().map(str::to_owned), + }); + } + anyhow::bail!("no environments available") +} + +fn pick_environment_row( + envs: &[CodeEnvironment], + desired_label: Option<&str>, +) -> Option { + if envs.is_empty() { + return None; + } + if let Some(label) = desired_label { + let lc = label.to_lowercase(); + if let Some(e) = envs + .iter() + .find(|e| e.label.as_deref().unwrap_or("").to_lowercase() == lc) + { + crate::append_error_log(format!("env: matched by label: {label} -> {}", e.id)); + return Some(e.clone()); + } + } + if envs.len() == 1 { + crate::append_error_log("env: single environment available; selecting it"); + return Some(envs[0].clone()); + } + if let Some(e) = envs.iter().find(|e| e.is_pinned.unwrap_or(false)) { + crate::append_error_log(format!("env: selecting pinned environment: {}", e.id)); + return Some(e.clone()); + } + // Highest task_count as heuristic + if let Some(e) = envs + .iter() + .max_by_key(|e| e.task_count.unwrap_or(0)) + .or_else(|| envs.first()) + { + crate::append_error_log(format!("env: selecting by task_count/first: {}", e.id)); + return Some(e.clone()); + } + None +} + +async fn get_json( + url: &str, + headers: &HeaderMap, +) -> anyhow::Result { + let http = reqwest::Client::builder().build()?; + let res = http.get(url).headers(headers.clone()).send().await?; + let status = res.status(); + let ct = res + .headers() + .get(CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + let body = res.text().await.unwrap_or_default(); + crate::append_error_log(format!("env: status={status} content-type={ct}")); + if !status.is_success() { + anyhow::bail!("GET {url} failed: {status}; content-type={ct}; body={body}"); + } + let parsed = serde_json::from_str::(&body).map_err(|e| { + anyhow::anyhow!("Decode error for {url}: {e}; content-type={ct}; body={body}") + })?; + Ok(parsed) +} + +fn get_git_origins() -> Vec { + // Prefer: git config --get-regexp remote\..*\.url + let out = std::process::Command::new("git") + .args(["config", "--get-regexp", "remote\\..*\\.url"]) + .output(); + if let Ok(ok) = out + && ok.status.success() + { + let s = String::from_utf8_lossy(&ok.stdout); + let mut urls = Vec::new(); + for line in s.lines() { + if let Some((_, url)) = line.split_once(' ') { + urls.push(url.trim().to_string()); + } + } + if !urls.is_empty() { + return uniq(urls); + } + } + // Fallback: git remote -v + let out = std::process::Command::new("git") + .args(["remote", "-v"]) + .output(); + if let Ok(ok) = out + && ok.status.success() + { + let s = String::from_utf8_lossy(&ok.stdout); + let mut urls = Vec::new(); + for line in s.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + urls.push(parts[1].to_string()); + } + } + if !urls.is_empty() { + return uniq(urls); + } + } + Vec::new() +} + +fn uniq(mut v: Vec) -> Vec { + v.sort(); + v.dedup(); + v +} + +fn parse_owner_repo(url: &str) -> Option<(String, String)> { + // Normalize common prefixes and handle multiple SSH/HTTPS variants. + let mut s = url.trim().to_string(); + // Drop protocol scheme for ssh URLs + if let Some(rest) = s.strip_prefix("ssh://") { + s = rest.to_string(); + } + // Accept any user before @github.com (e.g., git@, org-123@) + if let Some(idx) = s.find("@github.com:") { + let rest = &s[idx + "@github.com:".len()..]; + let rest = rest.trim_start_matches('/').trim_end_matches(".git"); + let mut parts = rest.splitn(2, '/'); + let owner = parts.next()?.to_string(); + let repo = parts.next()?.to_string(); + crate::append_error_log(format!("env: parsed SSH GitHub origin => {owner}/{repo}")); + return Some((owner, repo)); + } + // HTTPS or git protocol + for prefix in [ + "https://github.com/", + "http://github.com/", + "git://github.com/", + "github.com/", + ] { + if let Some(rest) = s.strip_prefix(prefix) { + let rest = rest.trim_start_matches('/').trim_end_matches(".git"); + let mut parts = rest.splitn(2, '/'); + let owner = parts.next()?.to_string(); + let repo = parts.next()?.to_string(); + crate::append_error_log(format!("env: parsed HTTP GitHub origin => {owner}/{repo}")); + return Some((owner, repo)); + } + } + None +} + +/// List environments for the current repo(s) with a fallback to the global list. +/// Returns a de-duplicated, sorted set suitable for the TUI modal. +pub async fn list_environments( + base_url: &str, + headers: &HeaderMap, +) -> anyhow::Result> { + let mut map: HashMap = HashMap::new(); + + // 1) By-repo lookup for each parsed GitHub origin + let origins = get_git_origins(); + for origin in &origins { + if let Some((owner, repo)) = parse_owner_repo(origin) { + let url = if base_url.contains("/backend-api") { + format!( + "{}/wham/environments/by-repo/{}/{}/{}", + base_url, "github", owner, repo + ) + } else { + format!( + "{}/api/codex/environments/by-repo/{}/{}/{}", + base_url, "github", owner, repo + ) + }; + match get_json::>(&url, headers).await { + Ok(list) => { + info!("env_tui: by-repo {}:{} -> {} envs", owner, repo, list.len()); + for e in list { + let entry = + map.entry(e.id.clone()) + .or_insert_with(|| crate::app::EnvironmentRow { + id: e.id.clone(), + label: e.label.clone(), + is_pinned: e.is_pinned.unwrap_or(false), + repo_hints: Some(format!("{owner}/{repo}")), + }); + // Merge: keep label if present, or use new; accumulate pinned flag + if entry.label.is_none() { + entry.label = e.label.clone(); + } + entry.is_pinned = entry.is_pinned || e.is_pinned.unwrap_or(false); + if entry.repo_hints.is_none() { + entry.repo_hints = Some(format!("{owner}/{repo}")); + } + } + } + Err(e) => { + warn!( + "env_tui: by-repo fetch failed for {}/{}: {}", + owner, repo, e + ); + } + } + } + } + + // 2) Fallback to the full list; on error return what we have if any. + let list_url = if base_url.contains("/backend-api") { + format!("{base_url}/wham/environments") + } else { + format!("{base_url}/api/codex/environments") + }; + match get_json::>(&list_url, headers).await { + Ok(list) => { + info!("env_tui: global list -> {} envs", list.len()); + for e in list { + let entry = map + .entry(e.id.clone()) + .or_insert_with(|| crate::app::EnvironmentRow { + id: e.id.clone(), + label: e.label.clone(), + is_pinned: e.is_pinned.unwrap_or(false), + repo_hints: None, + }); + if entry.label.is_none() { + entry.label = e.label.clone(); + } + entry.is_pinned = entry.is_pinned || e.is_pinned.unwrap_or(false); + } + } + Err(e) => { + if map.is_empty() { + return Err(e); + } else { + warn!( + "env_tui: global list failed; using by-repo results only: {}", + e + ); + } + } + } + + let mut rows: Vec = map.into_values().collect(); + rows.sort_by(|a, b| { + // pinned first + let p = b.is_pinned.cmp(&a.is_pinned); + if p != std::cmp::Ordering::Equal { + return p; + } + // then label (ci), then id + let al = a.label.as_deref().unwrap_or("").to_lowercase(); + let bl = b.label.as_deref().unwrap_or("").to_lowercase(); + let l = al.cmp(&bl); + if l != std::cmp::Ordering::Equal { + return l; + } + a.id.cmp(&b.id) + }); + Ok(rows) +} diff --git a/codex-rs/cloud-tasks/src/lib.rs b/codex-rs/cloud-tasks/src/lib.rs new file mode 100644 index 0000000000..d57a40572c --- /dev/null +++ b/codex-rs/cloud-tasks/src/lib.rs @@ -0,0 +1,1631 @@ +mod app; +mod cli; +pub mod env_detect; +mod new_task; +pub mod scrollable_diff; +mod ui; +pub mod util; +pub use cli::Cli; + +use std::io::IsTerminal; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use std::time::Instant; +use tokio::sync::mpsc::UnboundedSender; +use tracing::info; +use tracing_subscriber::EnvFilter; +use util::append_error_log; +use util::set_user_agent_suffix; + +struct ApplyJob { + task_id: codex_cloud_tasks_client::TaskId, + diff_override: Option, +} + +fn level_from_status(status: codex_cloud_tasks_client::ApplyStatus) -> app::ApplyResultLevel { + match status { + codex_cloud_tasks_client::ApplyStatus::Success => app::ApplyResultLevel::Success, + codex_cloud_tasks_client::ApplyStatus::Partial => app::ApplyResultLevel::Partial, + codex_cloud_tasks_client::ApplyStatus::Error => app::ApplyResultLevel::Error, + } +} + +fn spawn_preflight( + app: &mut app::App, + backend: &Arc, + tx: &UnboundedSender, + frame_tx: &UnboundedSender, + title: String, + job: ApplyJob, +) -> bool { + if app.apply_inflight { + app.status = "An apply is already running; wait for it to finish first.".to_string(); + return false; + } + if app.apply_preflight_inflight { + app.status = "A preflight is already running; wait for it to finish first.".to_string(); + return false; + } + + app.apply_preflight_inflight = true; + let _ = frame_tx.send(Instant::now() + Duration::from_millis(100)); + + let backend = backend.clone(); + let tx = tx.clone(); + tokio::spawn(async move { + let ApplyJob { + task_id, + diff_override, + } = job; + let result = codex_cloud_tasks_client::CloudBackend::apply_task_preflight( + &*backend, + task_id.clone(), + diff_override, + ) + .await; + + let event = match result { + Ok(outcome) => { + let level = level_from_status(outcome.status); + app::AppEvent::ApplyPreflightFinished { + id: task_id, + title, + message: outcome.message, + level, + skipped: outcome.skipped_paths, + conflicts: outcome.conflict_paths, + } + } + Err(e) => app::AppEvent::ApplyPreflightFinished { + id: task_id, + title, + message: format!("Preflight failed: {e}"), + level: app::ApplyResultLevel::Error, + skipped: Vec::new(), + conflicts: Vec::new(), + }, + }; + + let _ = tx.send(event); + }); + + true +} + +fn spawn_apply( + app: &mut app::App, + backend: &Arc, + tx: &UnboundedSender, + frame_tx: &UnboundedSender, + job: ApplyJob, +) -> bool { + if app.apply_inflight { + app.status = "An apply is already running; wait for it to finish first.".to_string(); + return false; + } + if app.apply_preflight_inflight { + app.status = "Finish the current preflight before starting another apply.".to_string(); + return false; + } + + app.apply_inflight = true; + let _ = frame_tx.send(Instant::now() + Duration::from_millis(100)); + + let backend = backend.clone(); + let tx = tx.clone(); + tokio::spawn(async move { + let ApplyJob { + task_id, + diff_override, + } = job; + let result = codex_cloud_tasks_client::CloudBackend::apply_task( + &*backend, + task_id.clone(), + diff_override, + ) + .await; + + let event = match result { + Ok(outcome) => app::AppEvent::ApplyFinished { + id: task_id, + result: Ok(outcome), + }, + Err(e) => app::AppEvent::ApplyFinished { + id: task_id, + result: Err(format!("{e}")), + }, + }; + + let _ = tx.send(event); + }); + + true +} + +// logging helper lives in util module + +// (no standalone patch summarizer needed – UI displays raw diffs) + +/// Entry point for the `codex cloud` subcommand. +pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option) -> anyhow::Result<()> { + // Very minimal logging setup; mirrors other crates' pattern. + let default_level = "error"; + let _ = tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new(default_level)) + .unwrap_or_else(|_| EnvFilter::new(default_level)), + ) + .with_ansi(std::io::stderr().is_terminal()) + .with_writer(std::io::stderr) + .try_init(); + + info!("Launching Cloud Tasks list UI"); + set_user_agent_suffix("codex_cloud_tasks_tui"); + + // Default to online unless explicitly configured to use mock. + let use_mock = matches!( + std::env::var("CODEX_CLOUD_TASKS_MODE").ok().as_deref(), + Some("mock") | Some("MOCK") + ); + + let backend: Arc = if use_mock { + Arc::new(codex_cloud_tasks_client::MockClient) + } else { + // Build an HTTP client against the configured (or default) base URL. + let base_url = std::env::var("CODEX_CLOUD_TASKS_BASE_URL") + .unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()); + let ua = codex_core::default_client::get_codex_user_agent(); + let mut http = + codex_cloud_tasks_client::HttpClient::new(base_url.clone())?.with_user_agent(ua); + // Log which base URL and path style we're going to use. + let style = if base_url.contains("/backend-api") { + "wham" + } else { + "codex-api" + }; + append_error_log(format!("startup: base_url={base_url} path_style={style}")); + + // Require ChatGPT login (SWIC). Exit with a clear message if missing. + let _token = match codex_core::config::find_codex_home() + .ok() + .map(codex_login::AuthManager::new) + .and_then(|am| am.auth()) + { + Some(auth) => { + // Log account context for debugging workspace selection. + if let Some(acc) = auth.get_account_id() { + append_error_log(format!("auth: mode=ChatGPT account_id={acc}")); + } + match auth.get_token().await { + Ok(t) if !t.is_empty() => { + // Attach token and ChatGPT-Account-Id header if available + http = http.with_bearer_token(t.clone()); + if let Some(acc) = auth + .get_account_id() + .or_else(|| util::extract_chatgpt_account_id(&t)) + { + append_error_log(format!("auth: set ChatGPT-Account-Id header: {acc}")); + http = http.with_chatgpt_account_id(acc); + } + t + } + _ => { + eprintln!( + "Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'." + ); + std::process::exit(1); + } + } + } + None => { + eprintln!( + "Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'." + ); + std::process::exit(1); + } + }; + Arc::new(http) + }; + + // Terminal setup + use crossterm::ExecutableCommand; + use crossterm::event::DisableBracketedPaste; + use crossterm::event::EnableBracketedPaste; + use crossterm::event::KeyboardEnhancementFlags; + use crossterm::event::PopKeyboardEnhancementFlags; + use crossterm::event::PushKeyboardEnhancementFlags; + use crossterm::terminal::EnterAlternateScreen; + use crossterm::terminal::LeaveAlternateScreen; + use crossterm::terminal::disable_raw_mode; + use crossterm::terminal::enable_raw_mode; + use ratatui::Terminal; + use ratatui::backend::CrosstermBackend; + let mut stdout = std::io::stdout(); + enable_raw_mode()?; + stdout.execute(EnterAlternateScreen)?; + stdout.execute(EnableBracketedPaste)?; + // Enable enhanced key reporting so Shift+Enter is distinguishable from Enter. + // Some terminals may not support these flags; ignore errors if enabling fails. + let _ = crossterm::execute!( + std::io::stdout(), + PushKeyboardEnhancementFlags( + KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES + | KeyboardEnhancementFlags::REPORT_EVENT_TYPES + | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS + ) + ); + let backend_ui = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend_ui)?; + terminal.clear()?; + + // App state + let mut app = app::App::new(); + // Initial load + let force_internal = matches!( + std::env::var("CODEX_CLOUD_TASKS_FORCE_INTERNAL") + .ok() + .as_deref(), + Some("1") | Some("true") | Some("TRUE") + ); + append_error_log(format!( + "startup: wham_force_internal={} ua={}", + force_internal, + codex_core::default_client::get_codex_user_agent() + )); + // Non-blocking initial load so the in-box spinner can animate + app.status = "Loading tasks…".to_string(); + app.refresh_inflight = true; + // New list generation; reset background enrichment coordination + app.list_generation = app.list_generation.saturating_add(1); + app.in_flight.clear(); + // reset any in-flight enrichment state + + // Event stream + use crossterm::event::Event; + use crossterm::event::EventStream; + use crossterm::event::KeyCode; + use crossterm::event::KeyEventKind; + use crossterm::event::KeyModifiers; + use tokio_stream::StreamExt; + let mut events = EventStream::new(); + + // Channel for non-blocking background loads + use tokio::sync::mpsc::unbounded_channel; + let (tx, mut rx) = unbounded_channel::(); + // Kick off the initial load in background + { + let backend = Arc::clone(&backend); + let tx = tx.clone(); + tokio::spawn(async move { + let res = app::load_tasks(&*backend, None).await; + let _ = tx.send(app::AppEvent::TasksLoaded { + env: None, + result: res, + }); + }); + } + // Fetch environment list in parallel so the header can show friendly names quickly. + { + let tx = tx.clone(); + tokio::spawn(async move { + let base_url = util::normalize_base_url( + &std::env::var("CODEX_CLOUD_TASKS_BASE_URL") + .unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()), + ); + let headers = util::build_chatgpt_headers().await; + let res = crate::env_detect::list_environments(&base_url, &headers).await; + let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res)); + }); + } + + // Try to auto-detect a likely environment id on startup and refresh if found. + // Do this concurrently so the initial list shows quickly; on success we refetch with filter. + { + let tx = tx.clone(); + tokio::spawn(async move { + let base_url = util::normalize_base_url( + &std::env::var("CODEX_CLOUD_TASKS_BASE_URL") + .unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()), + ); + // Build headers: UA + ChatGPT auth if available + let headers = util::build_chatgpt_headers().await; + + // Run autodetect. If it fails, we keep using "All". + let res = crate::env_detect::autodetect_environment_id(&base_url, &headers, None).await; + let _ = tx.send(app::AppEvent::EnvironmentAutodetected(res)); + }); + } + + // Event-driven redraws with a tiny coalescing scheduler (snappy UI, no fixed 250ms tick). + let mut needs_redraw = true; + use std::time::Instant; + use tokio::time::Instant as TokioInstant; + use tokio::time::sleep_until; + let (frame_tx, mut frame_rx) = tokio::sync::mpsc::unbounded_channel::(); + let (redraw_tx, mut redraw_rx) = tokio::sync::mpsc::unbounded_channel::<()>(); + + // Coalesce frame requests to the earliest deadline; emit a single redraw signal. + tokio::spawn(async move { + let mut next_deadline: Option = None; + loop { + let target = + next_deadline.unwrap_or_else(|| Instant::now() + Duration::from_secs(24 * 60 * 60)); + let sleeper = sleep_until(TokioInstant::from_std(target)); + tokio::pin!(sleeper); + tokio::select! { + recv = frame_rx.recv() => { + match recv { + Some(at) => { + if next_deadline.is_none_or(|cur| at < cur) { + next_deadline = Some(at); + } + continue; // recompute sleep target + } + None => break, + } + } + _ = &mut sleeper => { + if next_deadline.take().is_some() { + let _ = redraw_tx.send(()); + } + } + } + } + }); + // Kick an initial draw so the UI appears immediately. + let _ = frame_tx.send(Instant::now()); + + // Render helper to centralize immediate redraws after handling events. + let render_if_needed = |terminal: &mut Terminal>, + app: &mut app::App, + needs_redraw: &mut bool| + -> anyhow::Result<()> { + if *needs_redraw { + terminal.draw(|f| ui::draw(f, app))?; + *needs_redraw = false; + } + Ok(()) + }; + + let exit_code = loop { + tokio::select! { + // Coalesced redraw requests: spinner animation and paste-burst micro‑flush. + Some(()) = redraw_rx.recv() => { + // Micro‑flush pending first key held by paste‑burst. + if let Some(page) = app.new_task.as_mut() { + if page.composer.flush_paste_burst_if_due() { needs_redraw = true; } + if page.composer.is_in_paste_burst() { + let _ = frame_tx.send(Instant::now() + codex_tui::ComposerInput::recommended_flush_delay()); + } + } + // Advance throbber only while loading. + if app.refresh_inflight + || app.details_inflight + || app.env_loading + || app.apply_preflight_inflight + || app.apply_inflight + { + app.throbber.calc_next(); + needs_redraw = true; + let _ = frame_tx.send(Instant::now() + Duration::from_millis(100)); + } + render_if_needed(&mut terminal, &mut app, &mut needs_redraw)?; + } + maybe_app_event = rx.recv() => { + if let Some(ev) = maybe_app_event { + match ev { + app::AppEvent::TasksLoaded { env, result } => { + // Only apply results for the current filter to avoid races. + if env.as_deref() != app.env_filter.as_deref() { + append_error_log(format!( + "refresh.drop: env={} current={}", + env.clone().unwrap_or_else(|| "".to_string()), + app.env_filter.clone().unwrap_or_else(|| "".to_string()) + )); + continue; + } + app.refresh_inflight = false; + match result { + Ok(tasks) => { + append_error_log(format!( + "refresh.apply: env={} count={}", + env.clone().unwrap_or_else(|| "".to_string()), + tasks.len() + )); + app.tasks = tasks; + if app.selected >= app.tasks.len() { app.selected = app.tasks.len().saturating_sub(1); } + app.status = "Loaded tasks".to_string(); + } + Err(e) => { + append_error_log(format!("refresh load_tasks failed: {e}")); + app.status = format!("Failed to load tasks: {e}"); + } + } + needs_redraw = true; + let _ = frame_tx.send(Instant::now()); + } + app::AppEvent::NewTaskSubmitted(result) => { + match result { + Ok(created) => { + append_error_log(format!("new-task: created id={}", created.id.0)); + app.status = format!("Submitted as {}", created.id.0); + app.new_task = None; + // Refresh tasks in background for current filter + app.status = format!("Submitted as {} — refreshing…", created.id.0); + app.refresh_inflight = true; + app.list_generation = app.list_generation.saturating_add(1); + needs_redraw = true; + let backend = Arc::clone(&backend); + let tx = tx.clone(); + let env_sel = app.env_filter.clone(); + tokio::spawn(async move { + let res = app::load_tasks(&*backend, env_sel.as_deref()).await; + let _ = tx.send(app::AppEvent::TasksLoaded { env: env_sel, result: res }); + }); + let _ = frame_tx.send(Instant::now()); + } + Err(msg) => { + append_error_log(format!("new-task: submit failed: {msg}")); + if let Some(page) = app.new_task.as_mut() { page.submitting = false; } + app.status = format!("Submit failed: {msg}. See error.log for details."); + needs_redraw = true; + let _ = frame_tx.send(Instant::now()); + } + } + } + // (removed TaskSummaryUpdated; unused in this prototype) + app::AppEvent::ApplyPreflightFinished { id, title, message, level, skipped, conflicts } => { + // Only update if modal is still open and ids match + if let Some(m) = app.apply_modal.as_mut() + && m.task_id == id + { + m.title = title; + m.result_message = Some(message); + m.result_level = Some(level); + m.skipped_paths = skipped; + m.conflict_paths = conflicts; + app.apply_preflight_inflight = false; + needs_redraw = true; + let _ = frame_tx.send(Instant::now()); + } + } + app::AppEvent::EnvironmentsLoaded(result) => { + app.env_loading = false; + match result { + Ok(list) => { + app.environments = list; + app.env_error = None; + app.env_last_loaded = Some(std::time::Instant::now()); + } + Err(e) => { + app.env_error = Some(e.to_string()); + } + } + needs_redraw = true; + let _ = frame_tx.send(Instant::now()); + } + app::AppEvent::EnvironmentAutodetected(result) => { + if let Ok(sel) = result { + // Only apply if user hasn't set a filter yet or it's different. + if app.env_filter.as_deref() != Some(sel.id.as_str()) { + append_error_log(format!( + "env.select: autodetected id={} label={}", + sel.id, + sel.label.clone().unwrap_or_else(|| "".to_string()) + )); + // Preseed environments with detected label so header can show it even before list arrives + if let Some(lbl) = sel.label.clone() { + let present = app.environments.iter().any(|r| r.id == sel.id); + if !present { + app.environments.push(app::EnvironmentRow { id: sel.id.clone(), label: Some(lbl), is_pinned: false, repo_hints: None }); + } + } + app.env_filter = Some(sel.id); + app.status = "Loading tasks…".to_string(); + app.refresh_inflight = true; + app.list_generation = app.list_generation.saturating_add(1); + app.in_flight.clear(); + // reset spinner state + needs_redraw = true; + { + let backend = Arc::clone(&backend); + let tx = tx.clone(); + let env_sel = app.env_filter.clone(); + tokio::spawn(async move { + let res = app::load_tasks(&*backend, env_sel.as_deref()).await; + let _ = tx.send(app::AppEvent::TasksLoaded { env: env_sel, result: res }); + }); + } + // Proactively fetch environments to resolve a friendly name for the header. + app.env_loading = true; + { + let tx = tx.clone(); + tokio::spawn(async move { + let base_url = crate::util::normalize_base_url( + &std::env::var("CODEX_CLOUD_TASKS_BASE_URL") + .unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()), + ); + let headers = crate::util::build_chatgpt_headers().await; + let res = crate::env_detect::list_environments(&base_url, &headers).await; + let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res)); + }); + } + let _ = frame_tx.send(Instant::now()); + } + } + // on Err, silently continue with All + } + app::AppEvent::DetailsDiffLoaded { id, title, diff } => { + if let Some(ov) = &app.diff_overlay + && ov.task_id != id { + continue; + } + let diff_lines: Vec = diff.lines().map(str::to_string).collect(); + if let Some(ov) = app.diff_overlay.as_mut() { + ov.title = title; + { + let base = ov.base_attempt_mut(); + base.diff_lines = diff_lines.clone(); + base.diff_raw = Some(diff.clone()); + } + ov.base_can_apply = true; + ov.apply_selection_to_fields(); + } else { + let mut overlay = app::DiffOverlay::new(id.clone(), title, None); + { + let base = overlay.base_attempt_mut(); + base.diff_lines = diff_lines.clone(); + base.diff_raw = Some(diff.clone()); + } + overlay.base_can_apply = true; + overlay.current_view = app::DetailView::Diff; + overlay.apply_selection_to_fields(); + app.diff_overlay = Some(overlay); + } + app.details_inflight = false; + app.status.clear(); + needs_redraw = true; + } + app::AppEvent::DetailsMessagesLoaded { + id, + title, + messages, + prompt, + turn_id, + sibling_turn_ids, + attempt_placement, + attempt_status, + } => { + if let Some(ov) = &app.diff_overlay + && ov.task_id != id { + continue; + } + let conv = conversation_lines(prompt.clone(), &messages); + if let Some(ov) = app.diff_overlay.as_mut() { + ov.title = title.clone(); + { + let base = ov.base_attempt_mut(); + base.text_lines = conv.clone(); + base.prompt = prompt.clone(); + base.turn_id = turn_id.clone(); + base.status = attempt_status; + base.attempt_placement = attempt_placement; + } + ov.base_turn_id = turn_id.clone(); + ov.sibling_turn_ids = sibling_turn_ids.clone(); + ov.attempt_total_hint = Some(sibling_turn_ids.len().saturating_add(1)); + if !ov.base_can_apply { + ov.current_view = app::DetailView::Prompt; + } + ov.apply_selection_to_fields(); + if let (Some(turn_id), true) = (turn_id.clone(), !sibling_turn_ids.is_empty()) + && ov.attempts.len() == 1 { + let backend = Arc::clone(&backend); + let tx = tx.clone(); + let task_id = id.clone(); + tokio::spawn(async move { + match codex_cloud_tasks_client::CloudBackend::list_sibling_attempts( + &*backend, + task_id.clone(), + turn_id, + ) + .await + { + Ok(attempts) => { + let _ = tx.send(app::AppEvent::AttemptsLoaded { id: task_id, attempts }); + } + Err(e) => { + crate::util::append_error_log(format!( + "attempts.load failed for {}: {e}", + task_id.0 + )); + } + } + }); + } + } else { + let mut overlay = app::DiffOverlay::new(id.clone(), title, None); + { + let base = overlay.base_attempt_mut(); + base.text_lines = conv.clone(); + base.prompt = prompt.clone(); + base.turn_id = turn_id.clone(); + base.status = attempt_status; + base.attempt_placement = attempt_placement; + } + overlay.base_turn_id = turn_id.clone(); + overlay.sibling_turn_ids = sibling_turn_ids.clone(); + overlay.attempt_total_hint = Some(sibling_turn_ids.len().saturating_add(1)); + overlay.current_view = app::DetailView::Prompt; + overlay.apply_selection_to_fields(); + app.diff_overlay = Some(overlay); + } + app.details_inflight = false; + app.status.clear(); + needs_redraw = true; + } + app::AppEvent::AttemptsLoaded { id, attempts } => { + if let Some(ov) = app.diff_overlay.as_mut() { + if ov.task_id != id { + continue; + } + for attempt in attempts { + if ov + .attempts + .iter() + .any(|existing| existing.turn_id.as_deref() == Some(attempt.turn_id.as_str())) + { + continue; + } + let diff_lines = attempt + .diff + .as_ref() + .map(|d| d.lines().map(str::to_string).collect()) + .unwrap_or_default(); + let text_lines = conversation_lines(None, &attempt.messages); + ov.attempts.push(app::AttemptView { + turn_id: Some(attempt.turn_id.clone()), + status: attempt.status, + attempt_placement: attempt.attempt_placement, + diff_lines, + text_lines, + prompt: None, + diff_raw: attempt.diff.clone(), + }); + } + if ov.attempts.len() > 1 { + let (_, rest) = ov.attempts.split_at_mut(1); + rest.sort_by(|a, b| match (a.attempt_placement, b.attempt_placement) { + (Some(lhs), Some(rhs)) => lhs.cmp(&rhs), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => a.turn_id.cmp(&b.turn_id), + }); + } + if ov.selected_attempt >= ov.attempts.len() { + ov.selected_attempt = ov.attempts.len().saturating_sub(1); + } + ov.attempt_total_hint = Some(ov.attempts.len()); + ov.apply_selection_to_fields(); + needs_redraw = true; + } + } + app::AppEvent::DetailsFailed { id, title, error } => { + if let Some(ov) = &app.diff_overlay + && ov.task_id != id { + continue; + } + append_error_log(format!("details failed for {}: {error}", id.0)); + let pretty = pretty_lines_from_error(&error); + if let Some(ov) = app.diff_overlay.as_mut() { + ov.title = title.clone(); + { + let base = ov.base_attempt_mut(); + base.diff_lines.clear(); + base.text_lines = pretty.clone(); + base.prompt = None; + } + ov.base_can_apply = false; + ov.current_view = app::DetailView::Prompt; + ov.apply_selection_to_fields(); + } else { + let mut overlay = app::DiffOverlay::new(id.clone(), title, None); + { + let base = overlay.base_attempt_mut(); + base.text_lines = pretty; + } + overlay.base_can_apply = false; + overlay.current_view = app::DetailView::Prompt; + overlay.apply_selection_to_fields(); + app.diff_overlay = Some(overlay); + } + app.details_inflight = false; + needs_redraw = true; + } + app::AppEvent::ApplyFinished { id, result } => { + // Only update if the modal still corresponds to this id. + if let Some(m) = &app.apply_modal { + if m.task_id != id { continue; } + } else { + continue; + } + app.apply_inflight = false; + match result { + Ok(outcome) => { + app.status = outcome.message.clone(); + if matches!(outcome.status, codex_cloud_tasks_client::ApplyStatus::Success) { + app.apply_modal = None; + app.diff_overlay = None; + // Refresh tasks after successful apply + let backend = Arc::clone(&backend); + let tx = tx.clone(); + let env_sel = app.env_filter.clone(); + tokio::spawn(async move { + let res = app::load_tasks(&*backend, env_sel.as_deref()).await; + let _ = tx.send(app::AppEvent::TasksLoaded { env: env_sel, result: res }); + }); + } + } + Err(e) => { + append_error_log(format!("apply_task failed for {}: {e}", id.0)); + app.status = format!("Apply failed: {e}"); + } + } + needs_redraw = true; + } + } + } + // Render immediately after processing app events. + render_if_needed(&mut terminal, &mut app, &mut needs_redraw)?; + } + maybe_event = events.next() => { + match maybe_event { + Some(Ok(Event::Paste(pasted))) => { + if app.env_modal.is_some() { + if let Some(m) = app.env_modal.as_mut() { + for ch in pasted.chars() { + match ch { + '\r' | '\n' => continue, + '\t' => m.query.push(' '), + _ => m.query.push(ch), + } + } + } + needs_redraw = true; + } else if let Some(page) = app.new_task.as_mut() + && !page.submitting + { + if page.composer.handle_paste(pasted) { + needs_redraw = true; + } + let _ = frame_tx.send(Instant::now()); + } + } + Some(Ok(Event::Key(key))) if matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) => { + // Treat Ctrl-C like pressing 'q' in the current context. + if key.modifiers.contains(KeyModifiers::CONTROL) + && matches!(key.code, KeyCode::Char('c') | KeyCode::Char('C')) + { + if app.env_modal.is_some() { + // Close environment selector if open (don’t quit composer). + app.env_modal = None; + needs_redraw = true; + } else if app.best_of_modal.is_some() { + app.best_of_modal = None; + needs_redraw = true; + } else if app.apply_modal.is_some() { + app.apply_modal = None; + app.status = "Apply canceled".to_string(); + needs_redraw = true; + } else if app.new_task.is_some() { + app.new_task = None; + app.status = "Canceled new task".to_string(); + needs_redraw = true; + } else if app.diff_overlay.is_some() { + app.diff_overlay = None; + needs_redraw = true; + } else { + break 0; + } + // Render updated state immediately before continuing to next loop iteration. + render_if_needed(&mut terminal, &mut app, &mut needs_redraw)?; + // Render after New Task branch to reflect input changes immediately. + render_if_needed(&mut terminal, &mut app, &mut needs_redraw)?; + continue; + } + let is_ctrl_n = key.modifiers.contains(KeyModifiers::CONTROL) + && matches!(key.code, KeyCode::Char('n') | KeyCode::Char('N')) + || matches!(key.code, KeyCode::Char('\u{000E}')); + if is_ctrl_n { + if app.best_of_modal.is_some() { + app.best_of_modal = None; + needs_redraw = true; + } else { + let selected = app.best_of_n.saturating_sub(1).min(3); + app.best_of_modal = Some(app::BestOfModalState { selected }); + app.status = format!( + "Select best-of attempts (current: {} attempt{})", + app.best_of_n, + if app.best_of_n == 1 { "" } else { "s" } + ); + needs_redraw = true; + } + render_if_needed(&mut terminal, &mut app, &mut needs_redraw)?; + continue; + } + if app.best_of_modal.is_some() { + match key.code { + KeyCode::Esc => { + app.best_of_modal = None; + needs_redraw = true; + } + KeyCode::Down | KeyCode::Char('j') => { + if let Some(m) = app.best_of_modal.as_mut() { + m.selected = (m.selected + 1).min(3); + } + needs_redraw = true; + } + KeyCode::Up | KeyCode::Char('k') => { + if let Some(m) = app.best_of_modal.as_mut() { + m.selected = m.selected.saturating_sub(1); + } + needs_redraw = true; + } + KeyCode::Char('1') | KeyCode::Char('2') | KeyCode::Char('3') | KeyCode::Char('4') => { + if let Some(m) = app.best_of_modal.as_mut() { + let val = match key.code { + KeyCode::Char('1') => 0, + KeyCode::Char('2') => 1, + KeyCode::Char('3') => 2, + KeyCode::Char('4') => 3, + _ => m.selected, + }; + m.selected = val; + } + needs_redraw = true; + } + KeyCode::Enter => { + if let Some(state) = app.best_of_modal.take() { + let new_value = state.selected + 1; + app.best_of_n = new_value; + if let Some(page) = app.new_task.as_mut() { + page.best_of_n = new_value; + } + append_error_log(format!("best-of.select: attempts={new_value}")); + app.status = format!( + "Best-of updated to {new_value} attempt{}", + if new_value == 1 { "" } else { "s" } + ); + needs_redraw = true; + } + } + _ => {} + } + render_if_needed(&mut terminal, &mut app, &mut needs_redraw)?; + continue; + } + // New Task page: Ctrl+O opens environment switcher while composing. + let is_ctrl_o = key.modifiers.contains(KeyModifiers::CONTROL) + && matches!(key.code, KeyCode::Char('o') | KeyCode::Char('O')) + || matches!(key.code, KeyCode::Char('\u{000F}')); + if is_ctrl_o && app.new_task.is_some() { + // Close task modal/pending apply if present before opening env modal + app.diff_overlay = None; + app.env_modal = Some(app::EnvModalState { query: String::new(), selected: 0 }); + // Cache environments until user explicitly refreshes with 'r' inside the modal. + let should_fetch = app.environments.is_empty(); + if should_fetch { + app.env_loading = true; + app.env_error = None; + // Ensure spinner animates while loading environments. + let _ = frame_tx.send(Instant::now() + Duration::from_millis(100)); + } + needs_redraw = true; + if should_fetch { + let tx = tx.clone(); + tokio::spawn(async move { + let base_url = crate::util::normalize_base_url(&std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string())); + let headers = crate::util::build_chatgpt_headers().await; + let res = crate::env_detect::list_environments(&base_url, &headers).await; + let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res)); + }); + } + // Render after opening env modal to show it instantly. + render_if_needed(&mut terminal, &mut app, &mut needs_redraw)?; + continue; + } + + // New Task page has priority when active, unless an env modal is open. + if let Some(page) = app.new_task.as_mut() { + if app.env_modal.is_some() { + // Defer handling to env-modal branch below. + } else { + match key.code { + KeyCode::Esc => { + app.new_task = None; + app.status = "Canceled new task".to_string(); + needs_redraw = true; + } + _ => { + if page.submitting { + // Ignore input while submitting + } else if let codex_tui::ComposerAction::Submitted(text) = page.composer.input(key) { + // Submit only if we have an env id + if let Some(env) = page.env_id.clone() { + append_error_log(format!( + "new-task: submit env={} size={}", + env, + text.chars().count() + )); + page.submitting = true; + app.status = "Submitting new task…".to_string(); + let tx = tx.clone(); + let backend = Arc::clone(&backend); + let best_of_n = page.best_of_n; + tokio::spawn(async move { + let result = codex_cloud_tasks_client::CloudBackend::create_task(&*backend, &env, &text, "main", false, best_of_n).await; + let evt = match result { + Ok(ok) => app::AppEvent::NewTaskSubmitted(Ok(ok)), + Err(e) => app::AppEvent::NewTaskSubmitted(Err(format!("{e}"))), + }; + let _ = tx.send(evt); + }); + } else { + app.status = "No environment selected (press 'e' to choose)".to_string(); + } + } + needs_redraw = true; + // If paste‑burst is active, schedule a micro‑flush frame. + if page.composer.is_in_paste_burst() { + let _ = frame_tx.send(Instant::now() + codex_tui::ComposerInput::recommended_flush_delay()); + } + // Always schedule an immediate redraw for key edits in the composer. + let _ = frame_tx.send(Instant::now()); + // Draw now so non-char edits (e.g., Option+Delete) reflect instantly. + render_if_needed(&mut terminal, &mut app, &mut needs_redraw)?; + } + } + continue; + } + } + // If a diff overlay is open, handle its keys first. + if app.apply_modal.is_some() { + // Simple apply confirmation modal: y apply, p preflight, n/Esc cancel + match key.code { + KeyCode::Char('y') => { + if let Some(m) = app.apply_modal.as_ref() { + let title = m.title.clone(); + let job = ApplyJob { + task_id: m.task_id.clone(), + diff_override: m.diff_override.clone(), + }; + if spawn_apply(&mut app, &backend, &tx, &frame_tx, job) { + app.status = format!("Applying '{title}'..."); + } + needs_redraw = true; + } + } + KeyCode::Char('p') => { + if let Some(m) = app.apply_modal.take() { + let title = m.title.clone(); + let job = ApplyJob { + task_id: m.task_id.clone(), + diff_override: m.diff_override.clone(), + }; + if spawn_preflight(&mut app, &backend, &tx, &frame_tx, title.clone(), job) { + app.apply_modal = Some(app::ApplyModalState { + task_id: m.task_id, + title: title.clone(), + result_message: None, + result_level: None, + skipped_paths: Vec::new(), + conflict_paths: Vec::new(), + diff_override: m.diff_override, + }); + app.status = format!("Preflighting '{title}'..."); + } else { + app.apply_modal = Some(m); + } + needs_redraw = true; + } + } + KeyCode::Esc + | KeyCode::Char('n') + | KeyCode::Char('q') + | KeyCode::Char('Q') => { app.apply_modal = None; app.status = "Apply canceled".to_string(); needs_redraw = true; } + _ => {} + } + } else if app.diff_overlay.is_some() { + let mut cycle_attempt = |delta: isize| { + if let Some(ov) = app.diff_overlay.as_mut() + && ov.attempt_count() > 1 { + ov.step_attempt(delta); + let total = ov.attempt_display_total(); + let current = ov.selected_attempt + 1; + app.status = format!("Viewing attempt {current} of {total}"); + ov.sd.to_top(); + needs_redraw = true; + } + }; + + match key.code { + KeyCode::Char('a') => { + if app.apply_inflight || app.apply_preflight_inflight { + app.status = "Finish the current apply/preflight before starting another.".to_string(); + needs_redraw = true; + continue; + } + let snapshot = app.diff_overlay.as_ref().map(|ov| { + ( + ov.task_id.clone(), + ov.title.clone(), + ov.current_can_apply(), + ov.current_attempt().and_then(|attempt| attempt.diff_raw.clone()), + ) + }); + if let Some((task_id, title, can_apply, diff_override)) = snapshot { + if can_apply { + let job = ApplyJob { + task_id: task_id.clone(), + diff_override: diff_override.clone(), + }; + if spawn_preflight(&mut app, &backend, &tx, &frame_tx, title.clone(), job) { + app.apply_modal = Some(app::ApplyModalState { + task_id, + title: title.clone(), + result_message: None, + result_level: None, + skipped_paths: Vec::new(), + conflict_paths: Vec::new(), + diff_override, + }); + app.status = format!("Preflighting '{title}'..."); + } + } else { + app.status = "No diff available to apply.".to_string(); + } + needs_redraw = true; + } + } + KeyCode::Tab => { + cycle_attempt(1); + } + KeyCode::BackTab => { + cycle_attempt(-1); + } + // From task modal, 'o' should close it and open the env selector + KeyCode::Char('o') | KeyCode::Char('O') => { + app.diff_overlay = None; + app.env_modal = Some(app::EnvModalState { query: String::new(), selected: 0 }); + // Use cached environments unless empty + if app.environments.is_empty() { app.env_loading = true; app.env_error = None; } + needs_redraw = true; + if app.environments.is_empty() { + let tx = tx.clone(); + tokio::spawn(async move { + let base_url = crate::util::normalize_base_url( + &std::env::var("CODEX_CLOUD_TASKS_BASE_URL") + .unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()), + ); + let headers = crate::util::build_chatgpt_headers().await; + let res = crate::env_detect::list_environments(&base_url, &headers).await; + let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res)); + }); + } + } + KeyCode::Left => { + if let Some(ov) = &mut app.diff_overlay { + let has_text = ov.current_attempt().is_some_and(app::AttemptView::has_text); + let has_diff = ov.current_attempt().is_some_and(app::AttemptView::has_diff) || ov.base_can_apply; + if has_text && has_diff { + ov.set_view(app::DetailView::Prompt); + ov.sd.to_top(); + needs_redraw = true; + } + } + } + KeyCode::Right => { + if let Some(ov) = &mut app.diff_overlay { + let has_text = ov.current_attempt().is_some_and(app::AttemptView::has_text); + let has_diff = ov.current_attempt().is_some_and(app::AttemptView::has_diff) || ov.base_can_apply; + if has_text && has_diff { + ov.set_view(app::DetailView::Diff); + ov.sd.to_top(); + needs_redraw = true; + } + } + } + KeyCode::Char(']') | KeyCode::Char('}') => { + cycle_attempt(1); + } + KeyCode::Char('[') | KeyCode::Char('{') => { + cycle_attempt(-1); + } + KeyCode::Esc | KeyCode::Char('q') => { + app.diff_overlay = None; + needs_redraw = true; + } + KeyCode::Down | KeyCode::Char('j') => { + if let Some(ov) = &mut app.diff_overlay { ov.sd.scroll_by(1); } + needs_redraw = true; + } + KeyCode::Up | KeyCode::Char('k') => { + if let Some(ov) = &mut app.diff_overlay { ov.sd.scroll_by(-1); } + needs_redraw = true; + } + KeyCode::PageDown | KeyCode::Char(' ') => { + if let Some(ov) = &mut app.diff_overlay { let step = ov.sd.state.viewport_h.saturating_sub(1) as i16; ov.sd.page_by(step); } + needs_redraw = true; + } + KeyCode::PageUp => { + if let Some(ov) = &mut app.diff_overlay { let step = ov.sd.state.viewport_h.saturating_sub(1) as i16; ov.sd.page_by(-step); } + needs_redraw = true; + } + KeyCode::Home => { if let Some(ov) = &mut app.diff_overlay { ov.sd.to_top(); } needs_redraw = true; } + KeyCode::End => { if let Some(ov) = &mut app.diff_overlay { ov.sd.to_bottom(); } needs_redraw = true; } + _ => {} + } + } else if app.env_modal.is_some() { + // Environment modal key handling + match key.code { + KeyCode::Esc => { app.env_modal = None; needs_redraw = true; } + KeyCode::Char('r') | KeyCode::Char('R') => { + // Trigger refresh of environments + app.env_loading = true; app.env_error = None; needs_redraw = true; + let _ = frame_tx.send(Instant::now() + Duration::from_millis(100)); + let tx = tx.clone(); + tokio::spawn(async move { + let base_url = crate::util::normalize_base_url(&std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string())); + let headers = crate::util::build_chatgpt_headers().await; + let res = crate::env_detect::list_environments(&base_url, &headers).await; + let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res)); + }); + } + KeyCode::Char(ch) if !key.modifiers.contains(KeyModifiers::CONTROL) && !key.modifiers.contains(KeyModifiers::ALT) => { + if let Some(m) = app.env_modal.as_mut() { m.query.push(ch); } + needs_redraw = true; + } + KeyCode::Backspace => { if let Some(m) = app.env_modal.as_mut() { m.query.pop(); } needs_redraw = true; } + KeyCode::Down | KeyCode::Char('j') => { if let Some(m) = app.env_modal.as_mut() { m.selected = m.selected.saturating_add(1); } needs_redraw = true; } + KeyCode::Up | KeyCode::Char('k') => { if let Some(m) = app.env_modal.as_mut() { m.selected = m.selected.saturating_sub(1); } needs_redraw = true; } + KeyCode::Home => { if let Some(m) = app.env_modal.as_mut() { m.selected = 0; } needs_redraw = true; } + KeyCode::End => { if let Some(m) = app.env_modal.as_mut() { m.selected = app.environments.len(); } needs_redraw = true; } + KeyCode::PageDown | KeyCode::Char(' ') => { if let Some(m) = app.env_modal.as_mut() { let step = 10usize; m.selected = m.selected.saturating_add(step); } needs_redraw = true; } + KeyCode::PageUp => { if let Some(m) = app.env_modal.as_mut() { let step = 10usize; m.selected = m.selected.saturating_sub(step); } needs_redraw = true; } + KeyCode::Char('n') => { + if app.env_filter.is_none() { + app.new_task = Some(crate::new_task::NewTaskPage::new(None, app.best_of_n)); + } else { + app.new_task = Some(crate::new_task::NewTaskPage::new(app.env_filter.clone(), app.best_of_n)); + } + app.status = "New Task: Enter to submit; Esc to cancel".to_string(); + needs_redraw = true; + } + KeyCode::Enter => { + // Resolve selection over filtered set + if let Some(state) = app.env_modal.take() { + let q = state.query.to_lowercase(); + let filtered: Vec<&app::EnvironmentRow> = app.environments.iter().filter(|r| { + if q.is_empty() { return true; } + let mut hay = String::new(); + if let Some(l) = &r.label { hay.push_str(&l.to_lowercase()); hay.push(' '); } + hay.push_str(&r.id.to_lowercase()); + if let Some(h) = &r.repo_hints { hay.push(' '); hay.push_str(&h.to_lowercase()); } + hay.contains(&q) + }).collect(); + // Keep original order (already sorted) — no need to re-sort + let idx = state.selected; + if idx == 0 { app.env_filter = None; append_error_log("env.select: All"); } + else { + let env_idx = idx.saturating_sub(1); + if let Some(row) = filtered.get(env_idx) { + append_error_log(format!( + "env.select: id={} label={}", + row.id, + row.label.clone().unwrap_or_else(|| "".to_string()) + )); + app.env_filter = Some(row.id.clone()); + } + } + // If New Task page is open, reflect the new selection in its header immediately. + if let Some(page) = app.new_task.as_mut() { + page.env_id = app.env_filter.clone(); + } + // Trigger tasks refresh with the selected filter + app.status = "Loading tasks…".to_string(); + app.refresh_inflight = true; + app.list_generation = app.list_generation.saturating_add(1); + app.in_flight.clear(); + // reset spinner state + needs_redraw = true; + let backend = Arc::clone(&backend); + let tx = tx.clone(); + let env_sel = app.env_filter.clone(); + tokio::spawn(async move { + let res = app::load_tasks(&*backend, env_sel.as_deref()).await; + let _ = tx.send(app::AppEvent::TasksLoaded { env: env_sel, result: res }); + }); + } + } + _ => {} + } + } else { + // Base list view keys + match key.code { + KeyCode::Char('q') | KeyCode::Esc => { + break 0; + } + KeyCode::Down | KeyCode::Char('j') => { + app.next(); + needs_redraw = true; + } + KeyCode::Up | KeyCode::Char('k') => { + app.prev(); + needs_redraw = true; + } + // Ensure 'r' does not refresh tasks when the env modal is open. + KeyCode::Char('r') | KeyCode::Char('R') => { + if app.env_modal.is_some() { break 0; } + append_error_log(format!( + "refresh.request: env={}", + app.env_filter.clone().unwrap_or_else(|| "".to_string()) + )); + app.status = "Refreshing…".to_string(); + app.refresh_inflight = true; + app.list_generation = app.list_generation.saturating_add(1); + app.in_flight.clear(); + // reset spinner state + needs_redraw = true; + // Spawn background refresh + let backend = Arc::clone(&backend); + let tx = tx.clone(); + let env_sel = app.env_filter.clone(); + tokio::spawn(async move { + let res = app::load_tasks(&*backend, env_sel.as_deref()).await; + let _ = tx.send(app::AppEvent::TasksLoaded { env: env_sel, result: res }); + }); + } + KeyCode::Char('o') | KeyCode::Char('O') => { + app.env_modal = Some(app::EnvModalState { query: String::new(), selected: 0 }); + // Cache environments until user explicitly refreshes with 'r' inside the modal. + let should_fetch = app.environments.is_empty(); + if should_fetch { app.env_loading = true; app.env_error = None; } + needs_redraw = true; + if should_fetch { + let tx = tx.clone(); + tokio::spawn(async move { + let base_url = crate::util::normalize_base_url(&std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string())); + let headers = crate::util::build_chatgpt_headers().await; + let res = crate::env_detect::list_environments(&base_url, &headers).await; + let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res)); + }); + } + } + KeyCode::Char('n') => { + let env_opt = app.env_filter.clone(); + app.new_task = Some(crate::new_task::NewTaskPage::new(env_opt, app.best_of_n)); + app.status = "New Task: Enter to submit; Esc to cancel".to_string(); + needs_redraw = true; + } + KeyCode::Enter => { + if let Some(task) = app.tasks.get(app.selected).cloned() { + app.status = format!("Loading details for {title}…", title = task.title); + app.details_inflight = true; + // Open empty overlay immediately; content arrives via events + let overlay = app::DiffOverlay::new( + task.id.clone(), + task.title.clone(), + task.attempt_total, + ); + app.diff_overlay = Some(overlay); + needs_redraw = true; + // Spawn background details load (diff first, then messages fallback) + let id = task.id.clone(); + let title = task.title.clone(); + { + let backend = Arc::clone(&backend); + let tx = tx.clone(); + let diff_id = id.clone(); + let diff_title = title.clone(); + tokio::spawn(async move { + match codex_cloud_tasks_client::CloudBackend::get_task_diff(&*backend, diff_id.clone()).await { + Ok(Some(diff)) => { + let _ = tx.send(app::AppEvent::DetailsDiffLoaded { id: diff_id, title: diff_title, diff }); + } + Ok(None) => { + match codex_cloud_tasks_client::CloudBackend::get_task_text(&*backend, diff_id.clone()).await { + Ok(text) => { + let evt = app::AppEvent::DetailsMessagesLoaded { + id: diff_id, + title: diff_title, + messages: text.messages, + prompt: text.prompt, + turn_id: text.turn_id, + sibling_turn_ids: text.sibling_turn_ids, + attempt_placement: text.attempt_placement, + attempt_status: text.attempt_status, + }; + let _ = tx.send(evt); + } + Err(e2) => { + let _ = tx.send(app::AppEvent::DetailsFailed { id: diff_id, title: diff_title, error: format!("{e2}") }); + } + } + } + Err(e) => { + append_error_log(format!("get_task_diff failed for {}: {e}", diff_id.0)); + match codex_cloud_tasks_client::CloudBackend::get_task_text(&*backend, diff_id.clone()).await { + Ok(text) => { + let evt = app::AppEvent::DetailsMessagesLoaded { + id: diff_id, + title: diff_title, + messages: text.messages, + prompt: text.prompt, + turn_id: text.turn_id, + sibling_turn_ids: text.sibling_turn_ids, + attempt_placement: text.attempt_placement, + attempt_status: text.attempt_status, + }; + let _ = tx.send(evt); + } + Err(e2) => { + let _ = tx.send(app::AppEvent::DetailsFailed { id: diff_id, title: diff_title, error: format!("{e2}") }); + } + } + } + } + }); + } + // Also fetch conversation text even when diff exists + { + let backend = Arc::clone(&backend); + let tx = tx.clone(); + let msg_id = id; + let msg_title = title; + tokio::spawn(async move { + if let Ok(text) = codex_cloud_tasks_client::CloudBackend::get_task_text(&*backend, msg_id.clone()).await { + let evt = app::AppEvent::DetailsMessagesLoaded { + id: msg_id, + title: msg_title, + messages: text.messages, + prompt: text.prompt, + turn_id: text.turn_id, + sibling_turn_ids: text.sibling_turn_ids, + attempt_placement: text.attempt_placement, + attempt_status: text.attempt_status, + }; + let _ = tx.send(evt); + } + }); + } + // Animate spinner while details load. + let _ = frame_tx.send(Instant::now() + Duration::from_millis(100)); + } + } + KeyCode::Char('a') => { + if app.apply_inflight || app.apply_preflight_inflight { + app.status = "Finish the current apply/preflight before starting another.".to_string(); + needs_redraw = true; + continue; + } + + if let Some(task) = app.tasks.get(app.selected).cloned() { + match codex_cloud_tasks_client::CloudBackend::get_task_diff(&*backend, task.id.clone()).await { + Ok(Some(diff)) => { + let diff_override = Some(diff.clone()); + let task_id = task.id.clone(); + let title = task.title.clone(); + let job = ApplyJob { + task_id: task_id.clone(), + diff_override: diff_override.clone(), + }; + if spawn_preflight( + &mut app, + &backend, + &tx, + &frame_tx, + title.clone(), + job, + ) { + app.apply_modal = Some(app::ApplyModalState { + task_id, + title: title.clone(), + result_message: None, + result_level: None, + skipped_paths: Vec::new(), + conflict_paths: Vec::new(), + diff_override, + }); + app.status = format!("Preflighting '{title}'..."); + } + } + Ok(None) | Err(_) => { + app.status = "No diff available to apply".to_string(); + } + } + needs_redraw = true; + } + } + _ => {} + } + } + // Render after handling a key event (when not quitting). + render_if_needed(&mut terminal, &mut app, &mut needs_redraw)?; + } + Some(Ok(Event::Resize(_, _))) => { + needs_redraw = true; + // Redraw immediately on resize for snappier UX. + render_if_needed(&mut terminal, &mut app, &mut needs_redraw)?; + } + Some(Err(_)) | None => {} + _ => {} + } + // Fallback: if any other event path requested a redraw, render now. + render_if_needed(&mut terminal, &mut app, &mut needs_redraw)?; + } + } + }; + + // Restore terminal + disable_raw_mode().ok(); + terminal.show_cursor().ok(); + let _ = crossterm::execute!(std::io::stdout(), DisableBracketedPaste); + // Best-effort restore of keyboard enhancement flags before leaving alt screen. + let _ = crossterm::execute!(std::io::stdout(), PopKeyboardEnhancementFlags); + let _ = crossterm::execute!(std::io::stdout(), LeaveAlternateScreen); + + if exit_code != 0 { + std::process::exit(exit_code); + } + Ok(()) +} + +// extract_chatgpt_account_id moved to util.rs + +/// Build plain-text conversation lines: a labeled user prompt followed by assistant messages. +fn conversation_lines(prompt: Option, messages: &[String]) -> Vec { + let mut out: Vec = Vec::new(); + if let Some(p) = prompt { + out.push("user:".to_string()); + for l in p.lines() { + out.push(l.to_string()); + } + out.push(String::new()); + } + if !messages.is_empty() { + out.push("assistant:".to_string()); + for (i, m) in messages.iter().enumerate() { + for l in m.lines() { + out.push(l.to_string()); + } + if i + 1 < messages.len() { + out.push(String::new()); + } + } + } + if out.is_empty() { + out.push("".to_string()); + } + out +} + +/// Convert a verbose HTTP error with embedded JSON body into concise, user-friendly lines +/// for the details overlay. Falls back to a short raw message when parsing fails. +fn pretty_lines_from_error(raw: &str) -> Vec { + let mut lines: Vec = Vec::new(); + let is_no_diff = raw.contains("No output_diff in response."); + let is_no_msgs = raw.contains("No assistant text messages in response."); + if is_no_diff { + lines.push("No diff available for this task.".to_string()); + } else if is_no_msgs { + lines.push("No assistant messages found for this task.".to_string()); + } else { + lines.push("Failed to load task details.".to_string()); + } + + // Try to parse the embedded JSON body: find the first '{' after " body=" and decode. + if let Some(body_idx) = raw.find(" body=") + && let Some(json_start_rel) = raw[body_idx..].find('{') + { + let json_start = body_idx + json_start_rel; + let json_str = raw[json_start..].trim(); + if let Ok(v) = serde_json::from_str::(json_str) { + // Prefer assistant turn context. + let turn = v + .get("current_assistant_turn") + .and_then(|x| x.as_object()) + .cloned() + .or_else(|| { + v.get("current_diff_task_turn") + .and_then(|x| x.as_object()) + .cloned() + }); + if let Some(t) = turn { + if let Some(err) = t.get("error").and_then(|e| e.as_object()) { + let code = err.get("code").and_then(|s| s.as_str()).unwrap_or(""); + let msg = err.get("message").and_then(|s| s.as_str()).unwrap_or(""); + if !code.is_empty() || !msg.is_empty() { + let summary = if code.is_empty() { + msg.to_string() + } else if msg.is_empty() { + code.to_string() + } else { + format!("{code}: {msg}") + }; + lines.push(format!("Assistant error: {summary}")); + } + } + if let Some(status) = t.get("turn_status").and_then(|s| s.as_str()) { + lines.push(format!("Status: {status}")); + } + if let Some(text) = t + .get("latest_event") + .and_then(|e| e.get("text")) + .and_then(|s| s.as_str()) + && !text.trim().is_empty() + { + lines.push(format!("Latest event: {}", text.trim())); + } + } + } + } + + if lines.len() == 1 { + // Parsing yielded nothing; include a trimmed, short raw message tail for context. + let tail = if raw.len() > 320 { + format!("{}…", &raw[..320]) + } else { + raw.to_string() + }; + lines.push(tail); + } else if lines.len() >= 2 { + // Add a hint to refresh when still in progress. + if lines.iter().any(|l| l.contains("in_progress")) { + lines.push("This task may still be running. Press 'r' to refresh.".to_string()); + } + // Avoid an empty overlay + lines.push(String::new()); + } + lines +} + +#[cfg(test)] +mod tests { + use codex_tui::ComposerAction; + use codex_tui::ComposerInput; + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + use ratatui::buffer::Buffer; + use ratatui::layout::Rect; + + #[test] + fn composer_input_renders_typed_characters() { + let mut composer = ComposerInput::new(); + let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE); + match composer.input(key) { + ComposerAction::Submitted(_) => panic!("unexpected submission"), + ComposerAction::None => {} + } + + let area = Rect::new(0, 0, 20, 5); + let mut buf = Buffer::empty(area); + composer.render_ref(area, &mut buf); + + let found = buf.content().iter().any(|cell| cell.symbol() == "a"); + assert!(found, "typed character was not rendered: {buf:?}"); + + composer.set_hint_items(vec![("⌃O", "env"), ("⌃C", "quit")]); + composer.render_ref(area, &mut buf); + let footer = buf + .content() + .iter() + .skip((area.width as usize) * (area.height as usize - 1)) + .map(ratatui::buffer::Cell::symbol) + .collect::>() + .join(""); + assert!(footer.contains("⌃O env")); + } +} diff --git a/codex-rs/cloud-tasks/src/new_task.rs b/codex-rs/cloud-tasks/src/new_task.rs new file mode 100644 index 0000000000..162fd3bb3a --- /dev/null +++ b/codex-rs/cloud-tasks/src/new_task.rs @@ -0,0 +1,35 @@ +use codex_tui::ComposerInput; + +pub struct NewTaskPage { + pub composer: ComposerInput, + pub submitting: bool, + pub env_id: Option, + pub best_of_n: usize, +} + +impl NewTaskPage { + pub fn new(env_id: Option, best_of_n: usize) -> Self { + let mut composer = ComposerInput::new(); + composer.set_hint_items(vec![ + ("⏎", "send"), + ("Shift+⏎", "newline"), + ("Ctrl+O", "env"), + ("Ctrl+N", "attempts"), + ("Ctrl+C", "quit"), + ]); + Self { + composer, + submitting: false, + env_id, + best_of_n, + } + } + + // Additional helpers can be added as usage evolves. +} + +impl Default for NewTaskPage { + fn default() -> Self { + Self::new(None, 1) + } +} diff --git a/codex-rs/cloud-tasks/src/scrollable_diff.rs b/codex-rs/cloud-tasks/src/scrollable_diff.rs new file mode 100644 index 0000000000..97dfb24895 --- /dev/null +++ b/codex-rs/cloud-tasks/src/scrollable_diff.rs @@ -0,0 +1,176 @@ +use unicode_width::UnicodeWidthChar; +use unicode_width::UnicodeWidthStr; + +/// Scroll position and geometry for a vertical scroll view. +#[derive(Clone, Copy, Debug, Default)] +pub struct ScrollViewState { + pub scroll: u16, + pub viewport_h: u16, + pub content_h: u16, +} + +impl ScrollViewState { + pub fn clamp(&mut self) { + let max_scroll = self.content_h.saturating_sub(self.viewport_h); + if self.scroll > max_scroll { + self.scroll = max_scroll; + } + } +} + +/// A simple, local scrollable view for diffs or message text. +/// +/// Owns raw lines, caches wrapped lines for a given width, and maintains +/// a small scroll state that is clamped whenever geometry shrinks. +#[derive(Clone, Debug, Default)] +pub struct ScrollableDiff { + raw: Vec, + wrapped: Vec, + wrapped_src_idx: Vec, + wrap_cols: Option, + pub state: ScrollViewState, +} + +impl ScrollableDiff { + pub fn new() -> Self { + Self::default() + } + + /// Replace the raw content lines. Does not rewrap immediately; call `set_width` next. + pub fn set_content(&mut self, lines: Vec) { + self.raw = lines; + self.wrapped.clear(); + self.wrapped_src_idx.clear(); + self.state.content_h = 0; + // Force rewrap on next set_width even if width is unchanged + self.wrap_cols = None; + } + + /// Set the wrap width. If changed, rebuild wrapped lines and clamp scroll. + pub fn set_width(&mut self, width: u16) { + if self.wrap_cols == Some(width) { + return; + } + self.wrap_cols = Some(width); + self.rewrap(width); + self.state.clamp(); + } + + /// Update viewport height and clamp scroll if needed. + pub fn set_viewport(&mut self, height: u16) { + self.state.viewport_h = height; + self.state.clamp(); + } + + /// Return the cached wrapped lines. Call `set_width` first when area changes. + pub fn wrapped_lines(&self) -> &[String] { + &self.wrapped + } + + pub fn wrapped_src_indices(&self) -> &[usize] { + &self.wrapped_src_idx + } + + pub fn raw_line_at(&self, idx: usize) -> &str { + self.raw.get(idx).map(String::as_str).unwrap_or("") + } + + /// Scroll by a signed delta; clamps to content. + pub fn scroll_by(&mut self, delta: i16) { + let s = self.state.scroll as i32 + delta as i32; + self.state.scroll = s.clamp(0, self.max_scroll() as i32) as u16; + } + + /// Page by a signed delta; typically viewport_h - 1. + pub fn page_by(&mut self, delta: i16) { + self.scroll_by(delta); + } + + pub fn to_top(&mut self) { + self.state.scroll = 0; + } + + pub fn to_bottom(&mut self) { + self.state.scroll = self.max_scroll(); + } + + /// Optional percent scrolled; None when not enough geometry is known. + pub fn percent_scrolled(&self) -> Option { + if self.state.content_h == 0 || self.state.viewport_h == 0 { + return None; + } + if self.state.content_h <= self.state.viewport_h { + return None; + } + let visible_bottom = self.state.scroll.saturating_add(self.state.viewport_h) as f32; + let pct = (visible_bottom / self.state.content_h as f32 * 100.0).round(); + Some(pct.clamp(0.0, 100.0) as u8) + } + + fn max_scroll(&self) -> u16 { + self.state.content_h.saturating_sub(self.state.viewport_h) + } + + fn rewrap(&mut self, width: u16) { + if width == 0 { + self.wrapped = self.raw.clone(); + self.state.content_h = self.wrapped.len() as u16; + return; + } + let max_cols = width as usize; + let mut out: Vec = Vec::new(); + let mut out_idx: Vec = Vec::new(); + for (raw_idx, raw) in self.raw.iter().enumerate() { + // Normalize tabs for width accounting (MVP: 4 spaces). + let raw = raw.replace('\t', " "); + if raw.is_empty() { + out.push(String::new()); + out_idx.push(raw_idx); + continue; + } + let mut line = String::new(); + let mut line_cols = 0usize; + let mut last_soft_idx: Option = None; // last whitespace or punctuation break + for (_i, ch) in raw.char_indices() { + if ch == '\n' { + out.push(std::mem::take(&mut line)); + out_idx.push(raw_idx); + line_cols = 0; + last_soft_idx = None; + continue; + } + let w = UnicodeWidthChar::width(ch).unwrap_or(0); + if line_cols.saturating_add(w) > max_cols { + if let Some(split) = last_soft_idx { + let (prefix, rest) = line.split_at(split); + out.push(prefix.trim_end().to_string()); + out_idx.push(raw_idx); + line = rest.trim_start().to_string(); + last_soft_idx = None; + // retry add current ch now that line may be shorter + } else if !line.is_empty() { + out.push(std::mem::take(&mut line)); + out_idx.push(raw_idx); + } + } + if ch.is_whitespace() + || matches!( + ch, + ',' | ';' | '.' | ':' | ')' | ']' | '}' | '|' | '/' | '?' | '!' | '-' | '_' + ) + { + last_soft_idx = Some(line.len()); + } + line.push(ch); + line_cols = UnicodeWidthStr::width(line.as_str()); + } + if !line.is_empty() { + out.push(line); + out_idx.push(raw_idx); + } + } + self.wrapped = out; + self.wrapped_src_idx = out_idx; + self.state.content_h = self.wrapped.len() as u16; + } +} diff --git a/codex-rs/cloud-tasks/src/ui.rs b/codex-rs/cloud-tasks/src/ui.rs new file mode 100644 index 0000000000..94fb3ad97d --- /dev/null +++ b/codex-rs/cloud-tasks/src/ui.rs @@ -0,0 +1,1048 @@ +use ratatui::layout::Constraint; +use ratatui::layout::Direction; +use ratatui::layout::Layout; +use ratatui::prelude::*; +use ratatui::style::Color; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::style::Stylize; +use ratatui::widgets::Block; +use ratatui::widgets::BorderType; +use ratatui::widgets::Borders; +use ratatui::widgets::Clear; +use ratatui::widgets::List; +use ratatui::widgets::ListItem; +use ratatui::widgets::ListState; +use ratatui::widgets::Padding; +use ratatui::widgets::Paragraph; +use std::sync::OnceLock; + +use crate::app::App; +use crate::app::AttemptView; +use chrono::Local; +use chrono::Utc; +use codex_cloud_tasks_client::AttemptStatus; +use codex_cloud_tasks_client::TaskStatus; +use codex_tui::render_markdown_text; + +pub fn draw(frame: &mut Frame, app: &mut App) { + let area = frame.area(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Min(1), // list + Constraint::Length(2), // two-line footer (help + status) + ]) + .split(area); + if app.new_task.is_some() { + draw_new_task_page(frame, chunks[0], app); + draw_footer(frame, chunks[1], app); + } else { + draw_list(frame, chunks[0], app); + draw_footer(frame, chunks[1], app); + } + + if app.diff_overlay.is_some() { + draw_diff_overlay(frame, area, app); + } + if app.env_modal.is_some() { + draw_env_modal(frame, area, app); + } + if app.best_of_modal.is_some() { + draw_best_of_modal(frame, area, app); + } + if app.apply_modal.is_some() { + draw_apply_modal(frame, area, app); + } +} + +// ===== Overlay helpers (geometry + styling) ===== +static ROUNDED: OnceLock = OnceLock::new(); + +fn rounded_enabled() -> bool { + *ROUNDED.get_or_init(|| { + std::env::var("CODEX_TUI_ROUNDED") + .ok() + .map(|v| v == "1") + .unwrap_or(true) + }) +} + +fn overlay_outer(area: Rect) -> Rect { + let outer_v = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(10), + Constraint::Percentage(80), + Constraint::Percentage(10), + ]) + .split(area)[1]; + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(10), + Constraint::Percentage(80), + Constraint::Percentage(10), + ]) + .split(outer_v)[1] +} + +fn overlay_block() -> Block<'static> { + let base = Block::default().borders(Borders::ALL); + let base = if rounded_enabled() { + base.border_type(BorderType::Rounded) + } else { + base + }; + base.padding(Padding::new(2, 2, 1, 1)) +} + +fn overlay_content(area: Rect) -> Rect { + overlay_block().inner(area) +} + +pub fn draw_new_task_page(frame: &mut Frame, area: Rect, app: &mut App) { + let title_spans = { + let mut spans: Vec = vec!["New Task".magenta().bold()]; + if let Some(id) = app + .new_task + .as_ref() + .and_then(|p| p.env_id.as_ref()) + .cloned() + { + spans.push(" • ".into()); + // Try to map id to label + let label = app + .environments + .iter() + .find(|r| r.id == id) + .and_then(|r| r.label.clone()) + .unwrap_or(id); + spans.push(label.dim()); + } else { + spans.push(" • ".into()); + spans.push("Env: none (press ctrl-o to choose)".red()); + } + if let Some(page) = app.new_task.as_ref() { + spans.push(" • ".into()); + let attempts = page.best_of_n; + let label = format!( + "{} attempt{}", + attempts, + if attempts == 1 { "" } else { "s" } + ); + spans.push(label.cyan()); + } + spans + }; + let block = Block::default() + .borders(Borders::ALL) + .title(Line::from(title_spans)); + + frame.render_widget(Clear, area); + frame.render_widget(block.clone(), area); + let content = block.inner(area); + + // Expand composer height up to (terminal height - 6), with a 3-line minimum. + let max_allowed = frame.area().height.saturating_sub(6).max(3); + let desired = app + .new_task + .as_ref() + .map(|p| p.composer.desired_height(content.width)) + .unwrap_or(3) + .clamp(3, max_allowed); + + // Anchor the composer to the bottom-left by allocating a flexible spacer + // above it and a fixed `desired`-height area for the composer. + let rows = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Length(desired)]) + .split(content); + let composer_area = rows[1]; + + if let Some(page) = app.new_task.as_ref() { + page.composer.render_ref(composer_area, frame.buffer_mut()); + // Composer renders its own footer hints; no extra row here. + } + + // Place cursor where composer wants it + if let Some(page) = app.new_task.as_ref() + && let Some((x, y)) = page.composer.cursor_pos(composer_area) + { + frame.set_cursor_position((x, y)); + } +} + +fn draw_list(frame: &mut Frame, area: Rect, app: &mut App) { + let items: Vec = app.tasks.iter().map(|t| render_task_item(app, t)).collect(); + + // Selection reflects the actual task index (no artificial spacer item). + let mut state = ListState::default().with_selected(Some(app.selected)); + // Dim task list when a modal/overlay is active to emphasize focus. + let dim_bg = app.env_modal.is_some() + || app.apply_modal.is_some() + || app.best_of_modal.is_some() + || app.diff_overlay.is_some(); + // Dynamic title includes current environment filter + let suffix_span = if let Some(ref id) = app.env_filter { + let label = app + .environments + .iter() + .find(|r| &r.id == id) + .and_then(|r| r.label.clone()) + .unwrap_or_else(|| "Selected".to_string()); + format!(" • {label}").dim() + } else { + " • All".dim() + }; + // Percent scrolled based on selection position in the list (0% at top, 100% at bottom). + let percent_span = if app.tasks.len() <= 1 { + " • 0%".dim() + } else { + let p = ((app.selected as f32) / ((app.tasks.len() - 1) as f32) * 100.0).round() as i32; + format!(" • {}%", p.clamp(0, 100)).dim() + }; + let title_line = { + let base = Line::from(vec!["Cloud Tasks".into(), suffix_span, percent_span]); + if dim_bg { + base.style(Style::default().add_modifier(Modifier::DIM)) + } else { + base + } + }; + let block = Block::default().borders(Borders::ALL).title(title_line); + // Render the outer block first + frame.render_widget(block.clone(), area); + // Draw list inside with a persistent top spacer row + let inner = block.inner(area); + let rows = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(1)]) + .split(inner); + let mut list = List::new(items) + .highlight_symbol("› ") + .highlight_style(Style::default().bold()); + if dim_bg { + list = list.style(Style::default().add_modifier(Modifier::DIM)); + } + frame.render_stateful_widget(list, rows[1], &mut state); + + // In-box spinner during initial/refresh loads + if app.refresh_inflight { + draw_centered_spinner(frame, inner, &mut app.throbber, "Loading tasks…"); + } +} + +fn draw_footer(frame: &mut Frame, area: Rect, app: &mut App) { + let mut help = vec![ + "↑/↓".dim(), + ": Move ".dim(), + "r".dim(), + ": Refresh ".dim(), + "Enter".dim(), + ": Open ".dim(), + ]; + // Apply hint; show disabled note when overlay is open without a diff. + if let Some(ov) = app.diff_overlay.as_ref() { + if !ov.current_can_apply() { + help.push("a".dim()); + help.push(": Apply (disabled) ".dim()); + } else { + help.push("a".dim()); + help.push(": Apply ".dim()); + } + if ov.attempt_count() > 1 { + help.push("Tab".dim()); + help.push(": Next attempt ".dim()); + help.push("[ ]".dim()); + help.push(": Cycle attempts ".dim()); + } + } else { + help.push("a".dim()); + help.push(": Apply ".dim()); + } + help.push("o : Set Env ".dim()); + help.push("Ctrl+N".dim()); + help.push(format!(": Attempts {}x ", app.best_of_n).dim()); + if app.new_task.is_some() { + help.push("(editing new task) ".dim()); + } else { + help.push("n : New Task ".dim()); + } + help.extend(vec!["q".dim(), ": Quit ".dim()]); + // Split footer area into two rows: help+spinner (top) and status (bottom) + let rows = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Length(1)]) + .split(area); + + // Top row: help text + spinner at right + let top = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Fill(1), Constraint::Length(18)]) + .split(rows[0]); + let para = Paragraph::new(Line::from(help)); + // Draw help text; avoid clearing the whole footer area every frame. + frame.render_widget(para, top[0]); + // Right side: spinner or clear the spinner area if idle to prevent stale glyphs. + if app.refresh_inflight + || app.details_inflight + || app.env_loading + || app.apply_preflight_inflight + || app.apply_inflight + { + draw_inline_spinner(frame, top[1], &mut app.throbber, "Loading…"); + } else { + frame.render_widget(Clear, top[1]); + } + + // Bottom row: status/log text across full width (single-line; sanitize newlines) + let mut status_line = app.status.replace('\n', " "); + if status_line.len() > 2000 { + // hard cap to avoid TUI noise + status_line.truncate(2000); + status_line.push('…'); + } + // Clear the status row to avoid trailing characters when the message shrinks. + frame.render_widget(Clear, rows[1]); + let status = Paragraph::new(status_line); + frame.render_widget(status, rows[1]); +} + +fn draw_diff_overlay(frame: &mut Frame, area: Rect, app: &mut App) { + let inner = overlay_outer(area); + if app.diff_overlay.is_none() { + return; + } + let ov_can_apply = app + .diff_overlay + .as_ref() + .map(super::app::DiffOverlay::current_can_apply) + .unwrap_or(false); + let is_error = app + .diff_overlay + .as_ref() + .and_then(|o| o.sd.wrapped_lines().first().cloned()) + .map(|s| s.trim_start().starts_with("Task failed:")) + .unwrap_or(false) + && !ov_can_apply; + let title = app + .diff_overlay + .as_ref() + .map(|o| o.title.clone()) + .unwrap_or_default(); + + // Title block + let title_ref = title.as_str(); + let mut title_spans: Vec = if is_error { + vec![ + "Details ".magenta(), + "[FAILED]".red().bold(), + " ".into(), + title_ref.magenta(), + ] + } else if ov_can_apply { + vec!["Diff: ".magenta(), title_ref.magenta()] + } else { + vec!["Details: ".magenta(), title_ref.magenta()] + }; + if let Some(p) = app + .diff_overlay + .as_ref() + .and_then(|o| o.sd.percent_scrolled()) + { + title_spans.push(" • ".dim()); + title_spans.push(format!("{p}%").dim()); + } + frame.render_widget(Clear, inner); + frame.render_widget( + overlay_block().title(Line::from(title_spans)).clone(), + inner, + ); + + // Content area and optional status bar + let content_full = overlay_content(inner); + let mut content_area = content_full; + if let Some(ov) = app.diff_overlay.as_mut() { + let has_text = ov.current_attempt().is_some_and(AttemptView::has_text); + let has_diff = ov.current_attempt().is_some_and(AttemptView::has_diff) || ov.base_can_apply; + if has_diff || has_text { + let rows = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(1)]) + .split(content_full); + // Status bar label + let mut spans: Vec = Vec::new(); + if has_diff && has_text { + let prompt_lbl = if matches!(ov.current_view, crate::app::DetailView::Prompt) { + "[Prompt]".magenta().bold() + } else { + "Prompt".dim() + }; + let diff_lbl = if matches!(ov.current_view, crate::app::DetailView::Diff) { + "[Diff]".magenta().bold() + } else { + "Diff".dim() + }; + spans.extend(vec![ + prompt_lbl, + " ".into(), + diff_lbl, + " ".into(), + "(← → to switch view)".dim(), + ]); + } else if has_text { + spans.push("Conversation".magenta().bold()); + } else { + spans.push("Diff".magenta().bold()); + } + if let Some(total) = ov.expected_attempts().or({ + if ov.attempts.is_empty() { + None + } else { + Some(ov.attempts.len()) + } + }) && total > 1 + { + spans.extend(vec![ + " ".into(), + format!("Attempt {}/{}", ov.selected_attempt + 1, total) + .bold() + .dim(), + " ".into(), + "(Tab/Shift-Tab or [ ] to cycle attempts)".dim(), + ]); + } + frame.render_widget(Paragraph::new(Line::from(spans)), rows[0]); + ov.sd.set_width(rows[1].width); + ov.sd.set_viewport(rows[1].height); + content_area = rows[1]; + } else { + ov.sd.set_width(content_full.width); + ov.sd.set_viewport(content_full.height); + content_area = content_full; + } + } + + // Styled content render + // Choose styling by the active view, not just presence of a diff + let is_diff_view = app + .diff_overlay + .as_ref() + .map(|o| matches!(o.current_view, crate::app::DetailView::Diff)) + .unwrap_or(false); + let styled_lines: Vec> = if is_diff_view { + let raw = app.diff_overlay.as_ref().map(|o| o.sd.wrapped_lines()); + raw.unwrap_or(&[]) + .iter() + .map(|l| style_diff_line(l)) + .collect() + } else { + app.diff_overlay + .as_ref() + .map(|o| style_conversation_lines(&o.sd, o.current_attempt())) + .unwrap_or_default() + }; + let raw_empty = app + .diff_overlay + .as_ref() + .map(|o| o.sd.wrapped_lines().is_empty()) + .unwrap_or(true); + if app.details_inflight && raw_empty { + draw_centered_spinner(frame, content_area, &mut app.throbber, "Loading details…"); + } else { + let scroll = app + .diff_overlay + .as_ref() + .map(|o| o.sd.state.scroll) + .unwrap_or(0); + let content = Paragraph::new(Text::from(styled_lines)).scroll((scroll, 0)); + frame.render_widget(content, content_area); + } +} + +pub fn draw_apply_modal(frame: &mut Frame, area: Rect, app: &mut App) { + use ratatui::widgets::Wrap; + let inner = overlay_outer(area); + let title = Line::from("Apply Changes?".magenta().bold()); + let block = overlay_block().title(title); + frame.render_widget(Clear, inner); + frame.render_widget(block.clone(), inner); + let content = overlay_content(inner); + + if let Some(m) = &app.apply_modal { + // Header + let header = Paragraph::new(Line::from( + format!("Apply '{}' ?", m.title).magenta().bold(), + )) + .wrap(Wrap { trim: true }); + // Footer instructions + let footer = + Paragraph::new(Line::from("Press Y to apply, P to preflight, N to cancel.").dim()) + .wrap(Wrap { trim: true }); + + // Split into header/body/footer + let rows = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Min(1), + Constraint::Length(1), + ]) + .split(content); + + frame.render_widget(header, rows[0]); + // Body: spinner while preflight/apply runs; otherwise show result message and path lists + if app.apply_preflight_inflight { + draw_centered_spinner(frame, rows[1], &mut app.throbber, "Checking…"); + } else if app.apply_inflight { + draw_centered_spinner(frame, rows[1], &mut app.throbber, "Applying…"); + } else if m.result_message.is_none() { + draw_centered_spinner(frame, rows[1], &mut app.throbber, "Loading…"); + } else if let Some(msg) = &m.result_message { + let mut body_lines: Vec = Vec::new(); + let first = match m.result_level { + Some(crate::app::ApplyResultLevel::Success) => msg.clone().green(), + Some(crate::app::ApplyResultLevel::Partial) => msg.clone().magenta(), + Some(crate::app::ApplyResultLevel::Error) => msg.clone().red(), + None => msg.clone().into(), + }; + body_lines.push(Line::from(first)); + + // On partial or error, show conflicts/skips if present + if !matches!(m.result_level, Some(crate::app::ApplyResultLevel::Success)) { + use ratatui::text::Span; + if !m.conflict_paths.is_empty() { + body_lines.push(Line::from("")); + body_lines.push( + Line::from(format!("Conflicts ({}):", m.conflict_paths.len())) + .red() + .bold(), + ); + for p in &m.conflict_paths { + body_lines + .push(Line::from(vec![" • ".into(), Span::raw(p.clone()).dim()])); + } + } + if !m.skipped_paths.is_empty() { + body_lines.push(Line::from("")); + body_lines.push( + Line::from(format!("Skipped ({}):", m.skipped_paths.len())) + .magenta() + .bold(), + ); + for p in &m.skipped_paths { + body_lines + .push(Line::from(vec![" • ".into(), Span::raw(p.clone()).dim()])); + } + } + } + let body = Paragraph::new(body_lines).wrap(Wrap { trim: true }); + frame.render_widget(body, rows[1]); + } + frame.render_widget(footer, rows[2]); + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ConversationSpeaker { + User, + Assistant, +} + +fn style_conversation_lines( + sd: &crate::scrollable_diff::ScrollableDiff, + attempt: Option<&AttemptView>, +) -> Vec> { + use ratatui::text::Span; + + let wrapped = sd.wrapped_lines(); + if wrapped.is_empty() { + return Vec::new(); + } + + let indices = sd.wrapped_src_indices(); + let mut styled: Vec> = Vec::new(); + let mut speaker: Option = None; + let mut in_code = false; + let mut last_src: Option = None; + let mut bullet_indent: Option = None; + + for (display, &src_idx) in wrapped.iter().zip(indices.iter()) { + let raw = sd.raw_line_at(src_idx); + let trimmed = raw.trim(); + let is_new_raw = last_src.map(|prev| prev != src_idx).unwrap_or(true); + + if trimmed.eq_ignore_ascii_case("user:") { + speaker = Some(ConversationSpeaker::User); + in_code = false; + bullet_indent = None; + styled.push(conversation_header_line(ConversationSpeaker::User, None)); + last_src = Some(src_idx); + continue; + } + if trimmed.eq_ignore_ascii_case("assistant:") { + speaker = Some(ConversationSpeaker::Assistant); + in_code = false; + bullet_indent = None; + styled.push(conversation_header_line( + ConversationSpeaker::Assistant, + attempt, + )); + last_src = Some(src_idx); + continue; + } + if raw.is_empty() { + let mut spans: Vec = Vec::new(); + if let Some(role) = speaker { + spans.push(conversation_gutter_span(role)); + } else { + spans.push(Span::raw(String::new())); + } + styled.push(Line::from(spans)); + last_src = Some(src_idx); + bullet_indent = None; + continue; + } + + if is_new_raw { + let trimmed_start = raw.trim_start(); + if trimmed_start.starts_with("```") { + in_code = !in_code; + bullet_indent = None; + } else if !in_code + && (trimmed_start.starts_with("- ") || trimmed_start.starts_with("* ")) + { + let indent = raw.chars().take_while(|c| c.is_whitespace()).count(); + bullet_indent = Some(indent); + } else if !in_code { + bullet_indent = None; + } + } + + let mut spans: Vec = Vec::new(); + if let Some(role) = speaker { + spans.push(conversation_gutter_span(role)); + } + + spans.extend(conversation_text_spans( + display, + in_code, + is_new_raw, + bullet_indent, + )); + + styled.push(Line::from(spans)); + last_src = Some(src_idx); + } + + if styled.is_empty() { + wrapped.iter().map(|l| Line::from(l.to_string())).collect() + } else { + styled + } +} + +fn conversation_header_line( + speaker: ConversationSpeaker, + attempt: Option<&AttemptView>, +) -> Line<'static> { + use ratatui::text::Span; + + let mut spans: Vec = vec!["╭ ".dim()]; + match speaker { + ConversationSpeaker::User => { + spans.push("User".cyan().bold()); + spans.push(" prompt".dim()); + } + ConversationSpeaker::Assistant => { + spans.push("Assistant".magenta().bold()); + spans.push(" response".dim()); + if let Some(attempt) = attempt + && let Some(status_span) = attempt_status_span(attempt.status) + { + spans.push(" • ".dim()); + spans.push(status_span); + } + } + } + Line::from(spans) +} + +fn conversation_gutter_span(speaker: ConversationSpeaker) -> ratatui::text::Span<'static> { + match speaker { + ConversationSpeaker::User => "│ ".cyan().dim(), + ConversationSpeaker::Assistant => "│ ".magenta().dim(), + } +} + +fn conversation_text_spans( + display: &str, + in_code: bool, + is_new_raw: bool, + bullet_indent: Option, +) -> Vec> { + use ratatui::text::Span; + + if in_code { + return vec![Span::styled( + display.to_string(), + Style::default().fg(Color::Cyan), + )]; + } + + let trimmed = display.trim_start(); + + if let Some(indent) = bullet_indent { + if is_new_raw { + let rest = trimmed.get(2..).unwrap_or("").trim_start(); + let mut spans: Vec = Vec::new(); + if indent > 0 { + spans.push(Span::raw(" ".repeat(indent))); + } + spans.push("• ".into()); + spans.push(Span::raw(rest.to_string())); + return spans; + } + let mut continuation = String::new(); + continuation.push_str(&" ".repeat(indent + 2)); + continuation.push_str(trimmed); + return vec![Span::raw(continuation)]; + } + + if is_new_raw + && (trimmed.starts_with("### ") || trimmed.starts_with("## ") || trimmed.starts_with("# ")) + { + return vec![Span::styled( + display.to_string(), + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + )]; + } + + let mut rendered = render_markdown_text(display); + if rendered.lines.is_empty() { + return vec![Span::raw(display.to_string())]; + } + // `render_markdown_text` can yield multiple lines when the input contains + // explicit breaks. We only expect a single line here; join the spans of the + // first rendered line for styling. + rendered.lines.remove(0).spans.into_iter().collect() +} + +fn attempt_status_span(status: AttemptStatus) -> Option> { + match status { + AttemptStatus::Completed => Some("Completed".green()), + AttemptStatus::Failed => Some("Failed".red().bold()), + AttemptStatus::InProgress => Some("In progress".magenta()), + AttemptStatus::Pending => Some("Pending".cyan()), + AttemptStatus::Cancelled => Some("Cancelled".dim()), + AttemptStatus::Unknown => None, + } +} + +fn style_diff_line(raw: &str) -> Line<'static> { + use ratatui::style::Color; + use ratatui::style::Modifier; + use ratatui::style::Style; + use ratatui::text::Span; + + if raw.starts_with("@@") { + return Line::from(vec![Span::styled( + raw.to_string(), + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + )]); + } + if raw.starts_with("+++") || raw.starts_with("---") { + return Line::from(vec![Span::styled( + raw.to_string(), + Style::default().add_modifier(Modifier::DIM), + )]); + } + if raw.starts_with('+') { + return Line::from(vec![Span::styled( + raw.to_string(), + Style::default().fg(Color::Green), + )]); + } + if raw.starts_with('-') { + return Line::from(vec![Span::styled( + raw.to_string(), + Style::default().fg(Color::Red), + )]); + } + Line::from(vec![Span::raw(raw.to_string())]) +} + +fn render_task_item(_app: &App, t: &codex_cloud_tasks_client::TaskSummary) -> ListItem<'static> { + let status = match t.status { + TaskStatus::Ready => "READY".green(), + TaskStatus::Pending => "PENDING".magenta(), + TaskStatus::Applied => "APPLIED".blue(), + TaskStatus::Error => "ERROR".red(), + }; + + // Title line: [STATUS] Title + let title = Line::from(vec![ + "[".into(), + status, + "] ".into(), + t.title.clone().into(), + ]); + + // Meta line: environment label and relative time (dim) + let mut meta: Vec = Vec::new(); + if let Some(lbl) = t.environment_label.as_ref().filter(|s| !s.is_empty()) { + meta.push(lbl.clone().dim()); + } + let when = format_relative_time(t.updated_at).dim(); + if !meta.is_empty() { + meta.push(" ".into()); + meta.push("•".dim()); + meta.push(" ".into()); + } + meta.push(when); + let meta_line = Line::from(meta); + + // Subline: summary when present; otherwise show "no diff" + let sub = if t.summary.files_changed > 0 + || t.summary.lines_added > 0 + || t.summary.lines_removed > 0 + { + let adds = t.summary.lines_added; + let dels = t.summary.lines_removed; + let files = t.summary.files_changed; + Line::from(vec![ + format!("+{adds}").green(), + "/".into(), + format!("−{dels}").red(), + " ".into(), + "•".dim(), + " ".into(), + format!("{files}").into(), + " ".into(), + "files".dim(), + ]) + } else { + Line::from("no diff".to_string().dim()) + }; + + // Insert a blank spacer line after the summary to separate tasks + let spacer = Line::from(""); + ListItem::new(vec![title, meta_line, sub, spacer]) +} + +fn format_relative_time(ts: chrono::DateTime) -> String { + let now = Utc::now(); + let mut secs = (now - ts).num_seconds(); + if secs < 0 { + secs = 0; + } + if secs < 60 { + return format!("{secs}s ago"); + } + let mins = secs / 60; + if mins < 60 { + return format!("{mins}m ago"); + } + let hours = mins / 60; + if hours < 24 { + return format!("{hours}h ago"); + } + let local = ts.with_timezone(&Local); + local.format("%b %e %H:%M").to_string() +} + +fn draw_inline_spinner( + frame: &mut Frame, + area: Rect, + state: &mut throbber_widgets_tui::ThrobberState, + label: &str, +) { + use ratatui::style::Style; + use throbber_widgets_tui::BRAILLE_EIGHT; + use throbber_widgets_tui::Throbber; + use throbber_widgets_tui::WhichUse; + let w = Throbber::default() + .label(label) + .style(Style::default().cyan()) + .throbber_style(Style::default().magenta().bold()) + .throbber_set(BRAILLE_EIGHT) + .use_type(WhichUse::Spin); + frame.render_stateful_widget(w, area, state); +} + +fn draw_centered_spinner( + frame: &mut Frame, + area: Rect, + state: &mut throbber_widgets_tui::ThrobberState, + label: &str, +) { + // Center a 1xN throbber within the given rect + let rows = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(50), + Constraint::Length(1), + Constraint::Percentage(49), + ]) + .split(area); + let cols = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(50), + Constraint::Length(18), + Constraint::Percentage(50), + ]) + .split(rows[1]); + draw_inline_spinner(frame, cols[1], state, label); +} + +// Styling helpers for diff rendering live inline where used. + +pub fn draw_env_modal(frame: &mut Frame, area: Rect, app: &mut App) { + use ratatui::widgets::Wrap; + + // Use shared overlay geometry and padding. + let inner = overlay_outer(area); + + // Title: primary only; move long hints to a subheader inside content. + let title = Line::from(vec!["Select Environment".magenta().bold()]); + let block = overlay_block().title(title); + + frame.render_widget(Clear, inner); + frame.render_widget(block.clone(), inner); + let content = overlay_content(inner); + + if app.env_loading { + draw_centered_spinner(frame, content, &mut app.throbber, "Loading environments…"); + return; + } + + // Layout: subheader + search + results list + let rows = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), // subheader + Constraint::Length(1), // search + Constraint::Min(1), // list + ]) + .split(content); + + // Subheader with usage hints (dim cyan) + let subheader = Paragraph::new(Line::from( + "Type to search, Enter select, Esc cancel; r refresh" + .cyan() + .dim(), + )) + .wrap(Wrap { trim: true }); + frame.render_widget(subheader, rows[0]); + + let query = app + .env_modal + .as_ref() + .map(|m| m.query.clone()) + .unwrap_or_default(); + let ql = query.to_lowercase(); + let search = Paragraph::new(format!("Search: {query}")).wrap(Wrap { trim: true }); + frame.render_widget(search, rows[1]); + + // Filter environments by query (case-insensitive substring over label/id/hints) + let envs: Vec<&crate::app::EnvironmentRow> = app + .environments + .iter() + .filter(|e| { + if ql.is_empty() { + return true; + } + let mut hay = String::new(); + if let Some(l) = &e.label { + hay.push_str(&l.to_lowercase()); + hay.push(' '); + } + hay.push_str(&e.id.to_lowercase()); + if let Some(h) = &e.repo_hints { + hay.push(' '); + hay.push_str(&h.to_lowercase()); + } + hay.contains(&ql) + }) + .collect(); + + let mut items: Vec = Vec::new(); + items.push(ListItem::new(Line::from("All Environments (Global)"))); + for env in envs.iter() { + let primary = env.label.clone().unwrap_or_else(|| "".to_string()); + let mut spans: Vec = vec![primary.into()]; + if env.is_pinned { + spans.push(" ".into()); + spans.push("PINNED".magenta().bold()); + } + spans.push(" ".into()); + spans.push(env.id.clone().dim()); + if let Some(hint) = &env.repo_hints { + spans.push(" ".into()); + spans.push(hint.clone().dim()); + } + items.push(ListItem::new(Line::from(spans))); + } + + let sel_desired = app.env_modal.as_ref().map(|m| m.selected).unwrap_or(0); + let sel = sel_desired.min(envs.len()); + let mut list_state = ListState::default().with_selected(Some(sel)); + let list = List::new(items) + .highlight_symbol("› ") + .highlight_style(Style::default().bold()) + .block(Block::default().borders(Borders::NONE)); + frame.render_stateful_widget(list, rows[2], &mut list_state); +} + +pub fn draw_best_of_modal(frame: &mut Frame, area: Rect, app: &mut App) { + use ratatui::widgets::Wrap; + + let inner = overlay_outer(area); + let title = Line::from(vec!["Parallel Attempts".magenta().bold()]); + let block = overlay_block().title(title); + + frame.render_widget(Clear, inner); + frame.render_widget(block.clone(), inner); + let content = overlay_content(inner); + + let rows = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(2), Constraint::Min(1)]) + .split(content); + + let hint = Paragraph::new(Line::from( + "Use ↑/↓ to choose, 1-4 jump; Enter confirm, Esc cancel" + .cyan() + .dim(), + )) + .wrap(Wrap { trim: true }); + frame.render_widget(hint, rows[0]); + + let selected = app.best_of_modal.as_ref().map(|m| m.selected).unwrap_or(0); + let options = [1usize, 2, 3, 4]; + let mut items: Vec = Vec::new(); + for &attempts in &options { + let mut spans: Vec = + vec![format!("{attempts} attempt{}", if attempts == 1 { "" } else { "s" }).into()]; + spans.push(" ".into()); + spans.push(format!("{attempts}x parallel").dim()); + if attempts == app.best_of_n { + spans.push(" ".into()); + spans.push("Current".magenta().bold()); + } + items.push(ListItem::new(Line::from(spans))); + } + let sel = selected.min(options.len().saturating_sub(1)); + let mut list_state = ListState::default().with_selected(Some(sel)); + let list = List::new(items) + .highlight_symbol("› ") + .highlight_style(Style::default().bold()) + .block(Block::default().borders(Borders::NONE)); + frame.render_stateful_widget(list, rows[1], &mut list_state); +} diff --git a/codex-rs/cloud-tasks/src/util.rs b/codex-rs/cloud-tasks/src/util.rs new file mode 100644 index 0000000000..9a610da244 --- /dev/null +++ b/codex-rs/cloud-tasks/src/util.rs @@ -0,0 +1,93 @@ +use base64::Engine as _; +use chrono::Utc; +use reqwest::header::HeaderMap; + +pub fn set_user_agent_suffix(suffix: &str) { + if let Ok(mut guard) = codex_core::default_client::USER_AGENT_SUFFIX.lock() { + guard.replace(suffix.to_string()); + } +} + +pub fn append_error_log(message: impl AsRef) { + let ts = Utc::now().to_rfc3339(); + if let Ok(mut f) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open("error.log") + { + use std::io::Write as _; + let _ = writeln!(f, "[{ts}] {}", message.as_ref()); + } +} + +/// Normalize the configured base URL to a canonical form used by the backend client. +/// - trims trailing '/' +/// - appends '/backend-api' for ChatGPT hosts when missing +pub fn normalize_base_url(input: &str) -> String { + let mut base_url = input.to_string(); + while base_url.ends_with('/') { + base_url.pop(); + } + if (base_url.starts_with("https://chatgpt.com") + || base_url.starts_with("https://chat.openai.com")) + && !base_url.contains("/backend-api") + { + base_url = format!("{base_url}/backend-api"); + } + base_url +} + +/// Extract the ChatGPT account id from a JWT token, when present. +pub fn extract_chatgpt_account_id(token: &str) -> Option { + let mut parts = token.split('.'); + let (_h, payload_b64, _s) = match (parts.next(), parts.next(), parts.next()) { + (Some(h), Some(p), Some(s)) if !h.is_empty() && !p.is_empty() && !s.is_empty() => (h, p, s), + _ => return None, + }; + let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(payload_b64) + .ok()?; + let v: serde_json::Value = serde_json::from_slice(&payload_bytes).ok()?; + v.get("https://api.openai.com/auth") + .and_then(|auth| auth.get("chatgpt_account_id")) + .and_then(|id| id.as_str()) + .map(str::to_string) +} + +/// Build headers for ChatGPT-backed requests: `User-Agent`, optional `Authorization`, +/// and optional `ChatGPT-Account-Id`. +pub async fn build_chatgpt_headers() -> HeaderMap { + use reqwest::header::AUTHORIZATION; + use reqwest::header::HeaderName; + use reqwest::header::HeaderValue; + use reqwest::header::USER_AGENT; + + set_user_agent_suffix("codex_cloud_tasks_tui"); + let ua = codex_core::default_client::get_codex_user_agent(); + let mut headers = HeaderMap::new(); + headers.insert( + USER_AGENT, + HeaderValue::from_str(&ua).unwrap_or(HeaderValue::from_static("codex-cli")), + ); + if let Ok(home) = codex_core::config::find_codex_home() { + let am = codex_login::AuthManager::new(home); + if let Some(auth) = am.auth() + && let Ok(tok) = auth.get_token().await + && !tok.is_empty() + { + let v = format!("Bearer {tok}"); + if let Ok(hv) = HeaderValue::from_str(&v) { + headers.insert(AUTHORIZATION, hv); + } + if let Some(acc) = auth + .get_account_id() + .or_else(|| extract_chatgpt_account_id(&tok)) + && let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id") + && let Ok(hv) = HeaderValue::from_str(&acc) + { + headers.insert(name, hv); + } + } + } + headers +} diff --git a/codex-rs/cloud-tasks/tests/env_filter.rs b/codex-rs/cloud-tasks/tests/env_filter.rs new file mode 100644 index 0000000000..8c737c6c28 --- /dev/null +++ b/codex-rs/cloud-tasks/tests/env_filter.rs @@ -0,0 +1,22 @@ +use codex_cloud_tasks_client::CloudBackend; +use codex_cloud_tasks_client::MockClient; + +#[tokio::test] +async fn mock_backend_varies_by_env() { + let client = MockClient; + + let root = CloudBackend::list_tasks(&client, None).await.unwrap(); + assert!(root.iter().any(|t| t.title.contains("Update README"))); + + let a = CloudBackend::list_tasks(&client, Some("env-A")) + .await + .unwrap(); + assert_eq!(a.len(), 1); + assert_eq!(a[0].title, "A: First"); + + let b = CloudBackend::list_tasks(&client, Some("env-B")) + .await + .unwrap(); + assert_eq!(b.len(), 2); + assert!(b[0].title.starts_with("B: ")); +} diff --git a/codex-rs/codex-backend-openapi-models/Cargo.toml b/codex-rs/codex-backend-openapi-models/Cargo.toml new file mode 100644 index 0000000000..811ee72d8f --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "codex-backend-openapi-models" +version = { workspace = true } +edition = "2024" + +[lib] +name = "codex_backend_openapi_models" +path = "src/lib.rs" + +# Important: generated code often violates our workspace lints. +# Allow unwrap/expect in this crate so the workspace builds cleanly +# after models are regenerated. +# Lint overrides are applied in src/lib.rs via crate attributes + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/codex-rs/codex-backend-openapi-models/src/lib.rs b/codex-rs/codex-backend-openapi-models/src/lib.rs new file mode 100644 index 0000000000..f9e6d52fa8 --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/lib.rs @@ -0,0 +1,6 @@ +#![allow(clippy::unwrap_used, clippy::expect_used)] + +// Re-export generated OpenAPI models. +// The regen script populates `src/models/*.rs` and writes `src/models/mod.rs`. +// This module intentionally contains no hand-written types. +pub mod models; diff --git a/codex-rs/codex-backend-openapi-models/src/models/code_task_details_response.rs b/codex-rs/codex-backend-openapi-models/src/models/code_task_details_response.rs new file mode 100644 index 0000000000..725b3a3737 --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/code_task_details_response.rs @@ -0,0 +1,42 @@ +/* + * codex-backend + * + * codex-backend + * + * The version of the OpenAPI document: 0.0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct CodeTaskDetailsResponse { + #[serde(rename = "task")] + pub task: Box, + #[serde(rename = "current_user_turn", skip_serializing_if = "Option::is_none")] + pub current_user_turn: Option>, + #[serde( + rename = "current_assistant_turn", + skip_serializing_if = "Option::is_none" + )] + pub current_assistant_turn: Option>, + #[serde( + rename = "current_diff_task_turn", + skip_serializing_if = "Option::is_none" + )] + pub current_diff_task_turn: Option>, +} + +impl CodeTaskDetailsResponse { + pub fn new(task: models::TaskResponse) -> CodeTaskDetailsResponse { + CodeTaskDetailsResponse { + task: Box::new(task), + current_user_turn: None, + current_assistant_turn: None, + current_diff_task_turn: None, + } + } +} diff --git a/codex-rs/codex-backend-openapi-models/src/models/external_pull_request_response.rs b/codex-rs/codex-backend-openapi-models/src/models/external_pull_request_response.rs new file mode 100644 index 0000000000..92b56db2ca --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/external_pull_request_response.rs @@ -0,0 +1,40 @@ +/* + * codex-backend + * + * codex-backend + * + * The version of the OpenAPI document: 0.0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ExternalPullRequestResponse { + #[serde(rename = "id")] + pub id: String, + #[serde(rename = "assistant_turn_id")] + pub assistant_turn_id: String, + #[serde(rename = "pull_request")] + pub pull_request: Box, + #[serde(rename = "codex_updated_sha", skip_serializing_if = "Option::is_none")] + pub codex_updated_sha: Option, +} + +impl ExternalPullRequestResponse { + pub fn new( + id: String, + assistant_turn_id: String, + pull_request: models::GitPullRequest, + ) -> ExternalPullRequestResponse { + ExternalPullRequestResponse { + id, + assistant_turn_id, + pull_request: Box::new(pull_request), + codex_updated_sha: None, + } + } +} diff --git a/codex-rs/codex-backend-openapi-models/src/models/git_pull_request.rs b/codex-rs/codex-backend-openapi-models/src/models/git_pull_request.rs new file mode 100644 index 0000000000..a7e995f3ef --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/git_pull_request.rs @@ -0,0 +1,77 @@ +/* + * codex-backend + * + * codex-backend + * + * The version of the OpenAPI document: 0.0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GitPullRequest { + #[serde(rename = "number")] + pub number: i32, + #[serde(rename = "url")] + pub url: String, + #[serde(rename = "state")] + pub state: String, + #[serde(rename = "merged")] + pub merged: bool, + #[serde(rename = "mergeable")] + pub mergeable: bool, + #[serde(rename = "draft", skip_serializing_if = "Option::is_none")] + pub draft: Option, + #[serde(rename = "title", skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(rename = "body", skip_serializing_if = "Option::is_none")] + pub body: Option, + #[serde(rename = "base", skip_serializing_if = "Option::is_none")] + pub base: Option, + #[serde(rename = "head", skip_serializing_if = "Option::is_none")] + pub head: Option, + #[serde(rename = "base_sha", skip_serializing_if = "Option::is_none")] + pub base_sha: Option, + #[serde(rename = "head_sha", skip_serializing_if = "Option::is_none")] + pub head_sha: Option, + #[serde(rename = "merge_commit_sha", skip_serializing_if = "Option::is_none")] + pub merge_commit_sha: Option, + #[serde(rename = "comments", skip_serializing_if = "Option::is_none")] + pub comments: Option, + #[serde(rename = "diff", skip_serializing_if = "Option::is_none")] + pub diff: Option, + #[serde(rename = "user", skip_serializing_if = "Option::is_none")] + pub user: Option, +} + +impl GitPullRequest { + pub fn new( + number: i32, + url: String, + state: String, + merged: bool, + mergeable: bool, + ) -> GitPullRequest { + GitPullRequest { + number, + url, + state, + merged, + mergeable, + draft: None, + title: None, + body: None, + base: None, + head: None, + base_sha: None, + head_sha: None, + merge_commit_sha: None, + comments: None, + diff: None, + user: None, + } + } +} diff --git a/codex-rs/codex-backend-openapi-models/src/models/mod.rs b/codex-rs/codex-backend-openapi-models/src/models/mod.rs new file mode 100644 index 0000000000..e2cb972f10 --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/mod.rs @@ -0,0 +1,22 @@ +// Curated minimal export list for current workspace usage. +// NOTE: This file was previously auto-generated by the OpenAPI generator. +// Currently export only the types referenced by the workspace +// The process for this will change + +pub mod code_task_details_response; +pub use self::code_task_details_response::CodeTaskDetailsResponse; + +pub mod task_response; +pub use self::task_response::TaskResponse; + +pub mod external_pull_request_response; +pub use self::external_pull_request_response::ExternalPullRequestResponse; + +pub mod git_pull_request; +pub use self::git_pull_request::GitPullRequest; + +pub mod task_list_item; +pub use self::task_list_item::TaskListItem; + +pub mod paginated_list_task_list_item_; +pub use self::paginated_list_task_list_item_::PaginatedListTaskListItem; diff --git a/codex-rs/codex-backend-openapi-models/src/models/paginated_list_task_list_item_.rs b/codex-rs/codex-backend-openapi-models/src/models/paginated_list_task_list_item_.rs new file mode 100644 index 0000000000..5af75afaab --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/paginated_list_task_list_item_.rs @@ -0,0 +1,30 @@ +/* + * codex-backend + * + * codex-backend + * + * The version of the OpenAPI document: 0.0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct PaginatedListTaskListItem { + #[serde(rename = "items")] + pub items: Vec, + #[serde(rename = "cursor", skip_serializing_if = "Option::is_none")] + pub cursor: Option, +} + +impl PaginatedListTaskListItem { + pub fn new(items: Vec) -> PaginatedListTaskListItem { + PaginatedListTaskListItem { + items, + cursor: None, + } + } +} diff --git a/codex-rs/codex-backend-openapi-models/src/models/task_list_item.rs b/codex-rs/codex-backend-openapi-models/src/models/task_list_item.rs new file mode 100644 index 0000000000..5f34738a43 --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/task_list_item.rs @@ -0,0 +1,63 @@ +/* + * codex-backend + * + * codex-backend + * + * The version of the OpenAPI document: 0.0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct TaskListItem { + #[serde(rename = "id")] + pub id: String, + #[serde(rename = "title")] + pub title: String, + #[serde( + rename = "has_generated_title", + skip_serializing_if = "Option::is_none" + )] + pub has_generated_title: Option, + #[serde(rename = "updated_at", skip_serializing_if = "Option::is_none")] + pub updated_at: Option, + #[serde(rename = "created_at", skip_serializing_if = "Option::is_none")] + pub created_at: Option, + #[serde( + rename = "task_status_display", + skip_serializing_if = "Option::is_none" + )] + pub task_status_display: Option>, + #[serde(rename = "archived")] + pub archived: bool, + #[serde(rename = "has_unread_turn")] + pub has_unread_turn: bool, + #[serde(rename = "pull_requests", skip_serializing_if = "Option::is_none")] + pub pull_requests: Option>, +} + +impl TaskListItem { + pub fn new( + id: String, + title: String, + has_generated_title: Option, + archived: bool, + has_unread_turn: bool, + ) -> TaskListItem { + TaskListItem { + id, + title, + has_generated_title, + updated_at: None, + created_at: None, + task_status_display: None, + archived, + has_unread_turn, + pull_requests: None, + } + } +} diff --git a/codex-rs/codex-backend-openapi-models/src/models/task_response.rs b/codex-rs/codex-backend-openapi-models/src/models/task_response.rs new file mode 100644 index 0000000000..6251b56b7e --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/task_response.rs @@ -0,0 +1,62 @@ +/* + * codex-backend + * + * codex-backend + * + * The version of the OpenAPI document: 0.0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct TaskResponse { + #[serde(rename = "id")] + pub id: String, + #[serde(rename = "created_at", skip_serializing_if = "Option::is_none")] + pub created_at: Option, + #[serde(rename = "title")] + pub title: String, + #[serde( + rename = "has_generated_title", + skip_serializing_if = "Option::is_none" + )] + pub has_generated_title: Option, + #[serde(rename = "current_turn_id", skip_serializing_if = "Option::is_none")] + pub current_turn_id: Option, + #[serde(rename = "has_unread_turn", skip_serializing_if = "Option::is_none")] + pub has_unread_turn: Option, + #[serde( + rename = "denormalized_metadata", + skip_serializing_if = "Option::is_none" + )] + pub denormalized_metadata: Option>, + #[serde(rename = "archived")] + pub archived: bool, + #[serde(rename = "external_pull_requests")] + pub external_pull_requests: Vec, +} + +impl TaskResponse { + pub fn new( + id: String, + title: String, + archived: bool, + external_pull_requests: Vec, + ) -> TaskResponse { + TaskResponse { + id, + created_at: None, + title, + has_generated_title: None, + current_turn_id: None, + has_unread_turn: None, + denormalized_metadata: None, + archived, + external_pull_requests, + } + } +} diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 7d61620488..865bc9251f 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -433,7 +433,7 @@ fn set_project_trusted_inner(doc: &mut DocumentMut, project_path: &Path) -> anyh .get_mut(project_key.as_str()) .and_then(|i| i.as_table_mut()) else { - return Err(anyhow::anyhow!("project table missing for {}", project_key)); + return Err(anyhow::anyhow!("project table missing for {project_key}")); }; proj_tbl.set_implicit(false); proj_tbl["trust_level"] = toml_edit::value("trusted"); diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index a488791c2b..9be7d9568f 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -196,8 +196,7 @@ impl McpConnectionManager { // Validate server name before spawning if !is_valid_mcp_server_name(&server_name) { let error = anyhow::anyhow!( - "invalid server name '{}': must match pattern ^[a-zA-Z0-9_-]+$", - server_name + "invalid server name '{server_name}': must match pattern ^[a-zA-Z0-9_-]+$" ); errors.insert(server_name, error); continue; diff --git a/codex-rs/git-apply/Cargo.toml b/codex-rs/git-apply/Cargo.toml new file mode 100644 index 0000000000..0c17ccc3a4 --- /dev/null +++ b/codex-rs/git-apply/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "codex-git-apply" +version = { workspace = true } +edition = "2024" + +[lib] +name = "codex_git_apply" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +once_cell = "1" +regex = "1" +tempfile = "3" + diff --git a/codex-rs/git-apply/src/lib.rs b/codex-rs/git-apply/src/lib.rs new file mode 100644 index 0000000000..5d146312d9 --- /dev/null +++ b/codex-rs/git-apply/src/lib.rs @@ -0,0 +1,698 @@ +use once_cell::sync::Lazy; +use regex::Regex; +use std::ffi::OsStr; +use std::io; +use std::path::Path; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct ApplyGitRequest { + pub cwd: PathBuf, + pub diff: String, + pub revert: bool, + pub preflight: bool, +} + +#[derive(Debug, Clone)] +pub struct ApplyGitResult { + pub exit_code: i32, + pub applied_paths: Vec, + pub skipped_paths: Vec, + pub conflicted_paths: Vec, + pub stdout: String, + pub stderr: String, + pub cmd_for_log: String, +} + +pub fn apply_git_patch(req: &ApplyGitRequest) -> io::Result { + let git_root = resolve_git_root(&req.cwd)?; + + // Write unified diff into a temporary file + let (tmpdir, patch_path) = write_temp_patch(&req.diff)?; + // Keep tmpdir alive until function end to ensure the file exists + let _guard = tmpdir; + + if req.revert && !req.preflight { + // Stage WT paths first to avoid index mismatch on revert. + stage_paths(&git_root, &req.diff)?; + } + + // Build git args + let mut args: Vec = vec!["apply".into(), "--3way".into()]; + if req.revert { + args.push("-R".into()); + } + + // Optional: additional git config via env knob (defaults OFF) + let mut cfg_parts: Vec = Vec::new(); + if let Ok(cfg) = std::env::var("CODEX_APPLY_GIT_CFG") { + for pair in cfg.split(',') { + let p = pair.trim(); + if p.is_empty() || !p.contains('=') { + continue; + } + cfg_parts.push("-c".into()); + cfg_parts.push(p.to_string()); + } + } + + args.push(patch_path.to_string_lossy().to_string()); + + // Optional preflight: dry-run only; do not modify working tree + if req.preflight { + let mut check_args = vec!["apply".to_string(), "--check".to_string()]; + if req.revert { + check_args.push("-R".to_string()); + } + check_args.push(patch_path.to_string_lossy().to_string()); + let rendered = render_command_for_log(&git_root, &cfg_parts, &check_args); + let (c_code, c_out, c_err) = run_git(&git_root, &cfg_parts, &check_args)?; + let (mut applied_paths, mut skipped_paths, mut conflicted_paths) = + parse_git_apply_output(&c_out, &c_err); + applied_paths.sort(); + applied_paths.dedup(); + skipped_paths.sort(); + skipped_paths.dedup(); + conflicted_paths.sort(); + conflicted_paths.dedup(); + return Ok(ApplyGitResult { + exit_code: c_code, + applied_paths, + skipped_paths, + conflicted_paths, + stdout: c_out, + stderr: c_err, + cmd_for_log: rendered, + }); + } + + let cmd_for_log = render_command_for_log(&git_root, &cfg_parts, &args); + let (code, stdout, stderr) = run_git(&git_root, &cfg_parts, &args)?; + + let (mut applied_paths, mut skipped_paths, mut conflicted_paths) = + parse_git_apply_output(&stdout, &stderr); + applied_paths.sort(); + applied_paths.dedup(); + skipped_paths.sort(); + skipped_paths.dedup(); + conflicted_paths.sort(); + conflicted_paths.dedup(); + + Ok(ApplyGitResult { + exit_code: code, + applied_paths, + skipped_paths, + conflicted_paths, + stdout, + stderr, + cmd_for_log, + }) +} + +fn resolve_git_root(cwd: &Path) -> io::Result { + let out = std::process::Command::new("git") + .arg("rev-parse") + .arg("--show-toplevel") + .current_dir(cwd) + .output()?; + let code = out.status.code().unwrap_or(-1); + if code != 0 { + return Err(io::Error::other(format!( + "not a git repository (exit {}): {}", + code, + String::from_utf8_lossy(&out.stderr) + ))); + } + let root = String::from_utf8_lossy(&out.stdout).trim().to_string(); + Ok(PathBuf::from(root)) +} + +fn write_temp_patch(diff: &str) -> io::Result<(tempfile::TempDir, PathBuf)> { + let dir = tempfile::tempdir()?; + let path = dir.path().join("patch.diff"); + std::fs::write(&path, diff)?; + Ok((dir, path)) +} + +fn run_git(cwd: &Path, git_cfg: &[String], args: &[String]) -> io::Result<(i32, String, String)> { + let mut cmd = std::process::Command::new("git"); + for p in git_cfg { + cmd.arg(p); + } + for a in args { + cmd.arg(a); + } + let out = cmd.current_dir(cwd).output()?; + let code = out.status.code().unwrap_or(-1); + let stdout = String::from_utf8_lossy(&out.stdout).into_owned(); + let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); + Ok((code, stdout, stderr)) +} + +fn quote_shell(s: &str) -> String { + let simple = s + .chars() + .all(|c| c.is_ascii_alphanumeric() || "-_.:/@%+".contains(c)); + if simple { + s.to_string() + } else { + format!("'{}'", s.replace('\'', "'\\''")) + } +} + +fn render_command_for_log(cwd: &Path, git_cfg: &[String], args: &[String]) -> String { + let mut parts: Vec = Vec::new(); + parts.push("git".to_string()); + for a in git_cfg { + parts.push(quote_shell(a)); + } + for a in args { + parts.push(quote_shell(a)); + } + format!( + "(cd {} && {})", + quote_shell(&cwd.display().to_string()), + parts.join(" ") + ) +} + +pub fn extract_paths_from_patch(diff_text: &str) -> Vec { + static RE: Lazy = Lazy::new(|| { + Regex::new(r"(?m)^diff --git a/(.*?) b/(.*)$") + .unwrap_or_else(|e| panic!("invalid regex: {e}")) + }); + let mut set = std::collections::BTreeSet::new(); + for caps in RE.captures_iter(diff_text) { + if let Some(a) = caps.get(1).map(|m| m.as_str()) + && a != "/dev/null" + && !a.trim().is_empty() + { + set.insert(a.to_string()); + } + if let Some(b) = caps.get(2).map(|m| m.as_str()) + && b != "/dev/null" + && !b.trim().is_empty() + { + set.insert(b.to_string()); + } + } + set.into_iter().collect() +} + +pub fn stage_paths(git_root: &Path, diff: &str) -> io::Result<()> { + let paths = extract_paths_from_patch(diff); + let mut existing: Vec = Vec::new(); + for p in paths { + let joined = git_root.join(&p); + if std::fs::symlink_metadata(&joined).is_ok() { + existing.push(p); + } + } + if existing.is_empty() { + return Ok(()); + } + let mut cmd = std::process::Command::new("git"); + cmd.arg("add"); + cmd.arg("--"); + for p in &existing { + cmd.arg(OsStr::new(p)); + } + let out = cmd.current_dir(git_root).output()?; + let _code = out.status.code().unwrap_or(-1); + // We do not hard fail staging; best-effort is OK. Return Ok even on non-zero. + Ok(()) +} + +// ============ Parser ported from VS Code (TS) ============ + +pub fn parse_git_apply_output( + stdout: &str, + stderr: &str, +) -> (Vec, Vec, Vec) { + let combined = [stdout, stderr] + .iter() + .filter(|s| !s.is_empty()) + .cloned() + .collect::>() + .join("\n"); + + let mut applied = std::collections::BTreeSet::new(); + let mut skipped = std::collections::BTreeSet::new(); + let mut conflicted = std::collections::BTreeSet::new(); + let mut last_seen_path: Option = None; + + fn add(set: &mut std::collections::BTreeSet, raw: &str) { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return; + } + let first = trimmed.chars().next().unwrap_or('\0'); + let last = trimmed.chars().last().unwrap_or('\0'); + let unquoted = if (first == '"' || first == '\'') && last == first && trimmed.len() >= 2 { + &trimmed[1..trimmed.len() - 1] + } else { + trimmed + }; + if !unquoted.is_empty() { + set.insert(unquoted.to_string()); + } + } + + static APPLIED_CLEAN: Lazy = + Lazy::new(|| regex_ci("^Applied patch(?: to)?\\s+(?P.+?)\\s+cleanly\\.?$")); + static APPLIED_CONFLICTS: Lazy = + Lazy::new(|| regex_ci("^Applied patch(?: to)?\\s+(?P.+?)\\s+with conflicts\\.?$")); + static APPLYING_WITH_REJECTS: Lazy = Lazy::new(|| { + regex_ci("^Applying patch\\s+(?P.+?)\\s+with\\s+\\d+\\s+rejects?\\.{0,3}$") + }); + static CHECKING_PATCH: Lazy = + Lazy::new(|| regex_ci("^Checking patch\\s+(?P.+?)\\.\\.\\.$")); + static UNMERGED_LINE: Lazy = Lazy::new(|| regex_ci("^U\\s+(?P.+)$")); + static PATCH_FAILED: Lazy = + Lazy::new(|| regex_ci("^error:\\s+patch failed:\\s+(?P.+?)(?::\\d+)?(?:\\s|$)")); + static DOES_NOT_APPLY: Lazy = + Lazy::new(|| regex_ci("^error:\\s+(?P.+?):\\s+patch does not apply$")); + static THREE_WAY_START: Lazy = Lazy::new(|| { + regex_ci("^(?:Performing three-way merge|Falling back to three-way merge)\\.\\.\\.$") + }); + static THREE_WAY_FAILED: Lazy = + Lazy::new(|| regex_ci("^Failed to perform three-way merge\\.\\.\\.$")); + static FALLBACK_DIRECT: Lazy = + Lazy::new(|| regex_ci("^Falling back to direct application\\.\\.\\.$")); + static LACKS_BLOB: Lazy = Lazy::new(|| { + regex_ci( + "^(?:error: )?repository lacks the necessary blob to (?:perform|fall back on) 3-?way merge\\.?$", + ) + }); + static INDEX_MISMATCH: Lazy = + Lazy::new(|| regex_ci("^error:\\s+(?P.+?):\\s+does not match index\\b")); + static NOT_IN_INDEX: Lazy = + Lazy::new(|| regex_ci("^error:\\s+(?P.+?):\\s+does not exist in index\\b")); + static ALREADY_EXISTS_WT: Lazy = Lazy::new(|| { + regex_ci("^error:\\s+(?P.+?)\\s+already exists in (?:the )?working directory\\b") + }); + static FILE_EXISTS: Lazy = + Lazy::new(|| regex_ci("^error:\\s+patch failed:\\s+(?P.+?)\\s+File exists")); + static RENAMED_DELETED: Lazy = + Lazy::new(|| regex_ci("^error:\\s+path\\s+(?P.+?)\\s+has been renamed\\/deleted")); + static CANNOT_APPLY_BINARY: Lazy = Lazy::new(|| { + regex_ci( + "^error:\\s+cannot apply binary patch to\\s+['\\\"]?(?P.+?)['\\\"]?\\s+without full index line$", + ) + }); + static BINARY_DOES_NOT_APPLY: Lazy = Lazy::new(|| { + regex_ci("^error:\\s+binary patch does not apply to\\s+['\\\"]?(?P.+?)['\\\"]?$") + }); + static BINARY_INCORRECT_RESULT: Lazy = Lazy::new(|| { + regex_ci( + "^error:\\s+binary patch to\\s+['\\\"]?(?P.+?)['\\\"]?\\s+creates incorrect result\\b", + ) + }); + static CANNOT_READ_CURRENT: Lazy = Lazy::new(|| { + regex_ci("^error:\\s+cannot read the current contents of\\s+['\\\"]?(?P.+?)['\\\"]?$") + }); + static SKIPPED_PATCH: Lazy = + Lazy::new(|| regex_ci("^Skipped patch\\s+['\\\"]?(?P.+?)['\\\"]\\.$")); + static CANNOT_MERGE_BINARY_WARN: Lazy = Lazy::new(|| { + regex_ci( + "^warning:\\s*Cannot merge binary files:\\s+(?P.+?)\\s+\\(ours\\s+vs\\.\\s+theirs\\)", + ) + }); + + for raw_line in combined.lines() { + let line = raw_line.trim(); + if line.is_empty() { + continue; + } + + // === "Checking patch ..." tracking === + if let Some(c) = CHECKING_PATCH.captures(line) { + if let Some(m) = c.name("path") { + last_seen_path = Some(m.as_str().to_string()); + } + continue; + } + + // === Status lines === + if let Some(c) = APPLIED_CLEAN.captures(line) { + if let Some(m) = c.name("path") { + add(&mut applied, m.as_str()); + let p = applied.iter().next_back().cloned(); + if let Some(p) = p { + conflicted.remove(&p); + skipped.remove(&p); + last_seen_path = Some(p); + } + } + continue; + } + if let Some(c) = APPLIED_CONFLICTS.captures(line) { + if let Some(m) = c.name("path") { + add(&mut conflicted, m.as_str()); + let p = conflicted.iter().next_back().cloned(); + if let Some(p) = p { + applied.remove(&p); + skipped.remove(&p); + last_seen_path = Some(p); + } + } + continue; + } + if let Some(c) = APPLYING_WITH_REJECTS.captures(line) { + if let Some(m) = c.name("path") { + add(&mut conflicted, m.as_str()); + let p = conflicted.iter().next_back().cloned(); + if let Some(p) = p { + applied.remove(&p); + skipped.remove(&p); + last_seen_path = Some(p); + } + } + continue; + } + + // === “U ” after conflicts === + if let Some(c) = UNMERGED_LINE.captures(line) { + if let Some(m) = c.name("path") { + add(&mut conflicted, m.as_str()); + let p = conflicted.iter().next_back().cloned(); + if let Some(p) = p { + applied.remove(&p); + skipped.remove(&p); + last_seen_path = Some(p); + } + } + continue; + } + + // === Early hints === + if PATCH_FAILED.is_match(line) || DOES_NOT_APPLY.is_match(line) { + if let Some(c) = PATCH_FAILED + .captures(line) + .or_else(|| DOES_NOT_APPLY.captures(line)) + && let Some(m) = c.name("path") + { + add(&mut skipped, m.as_str()); + last_seen_path = Some(m.as_str().to_string()); + } + continue; + } + + // === Ignore narration === + if THREE_WAY_START.is_match(line) || FALLBACK_DIRECT.is_match(line) { + continue; + } + + // === 3-way failed entirely; attribute to last_seen_path === + if THREE_WAY_FAILED.is_match(line) || LACKS_BLOB.is_match(line) { + if let Some(p) = last_seen_path.clone() { + add(&mut skipped, &p); + applied.remove(&p); + conflicted.remove(&p); + } + continue; + } + + // === Skips / I/O problems === + if let Some(c) = INDEX_MISMATCH + .captures(line) + .or_else(|| NOT_IN_INDEX.captures(line)) + .or_else(|| ALREADY_EXISTS_WT.captures(line)) + .or_else(|| FILE_EXISTS.captures(line)) + .or_else(|| RENAMED_DELETED.captures(line)) + .or_else(|| CANNOT_APPLY_BINARY.captures(line)) + .or_else(|| BINARY_DOES_NOT_APPLY.captures(line)) + .or_else(|| BINARY_INCORRECT_RESULT.captures(line)) + .or_else(|| CANNOT_READ_CURRENT.captures(line)) + .or_else(|| SKIPPED_PATCH.captures(line)) + { + if let Some(m) = c.name("path") { + add(&mut skipped, m.as_str()); + let p_now = skipped.iter().next_back().cloned(); + if let Some(p) = p_now { + applied.remove(&p); + conflicted.remove(&p); + last_seen_path = Some(p); + } + } + continue; + } + + // === Warnings that imply conflicts === + if let Some(c) = CANNOT_MERGE_BINARY_WARN.captures(line) { + if let Some(m) = c.name("path") { + add(&mut conflicted, m.as_str()); + let p = conflicted.iter().next_back().cloned(); + if let Some(p) = p { + applied.remove(&p); + skipped.remove(&p); + last_seen_path = Some(p); + } + } + continue; + } + } + + // Final precedence: conflicts > applied > skipped + for p in conflicted.iter() { + applied.remove(p); + skipped.remove(p); + } + for p in applied.iter() { + skipped.remove(p); + } + + ( + applied.into_iter().collect(), + skipped.into_iter().collect(), + conflicted.into_iter().collect(), + ) +} + +fn regex_ci(pat: &str) -> Regex { + Regex::new(&format!("(?i){pat}")).unwrap_or_else(|e| panic!("invalid regex: {e}")) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + use std::sync::Mutex; + use std::sync::OnceLock; + + fn env_lock() -> &'static Mutex<()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) + } + + fn run(cwd: &Path, args: &[&str]) -> (i32, String, String) { + let out = std::process::Command::new(args[0]) + .args(&args[1..]) + .current_dir(cwd) + .output() + .expect("spawn ok"); + ( + out.status.code().unwrap_or(-1), + String::from_utf8_lossy(&out.stdout).into_owned(), + String::from_utf8_lossy(&out.stderr).into_owned(), + ) + } + + fn init_repo() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + let root = dir.path(); + // git init and minimal identity + let _ = run(root, &["git", "init"]); + let _ = run(root, &["git", "config", "user.email", "codex@example.com"]); + let _ = run(root, &["git", "config", "user.name", "Codex"]); + dir + } + + fn read_file_normalized(path: &Path) -> String { + std::fs::read_to_string(path) + .expect("read file") + .replace("\r\n", "\n") + } + + #[test] + fn apply_add_success() { + let _g = env_lock().lock().unwrap(); + let repo = init_repo(); + let root = repo.path(); + + let diff = "diff --git a/hello.txt b/hello.txt\nnew file mode 100644\n--- /dev/null\n+++ b/hello.txt\n@@ -0,0 +1,2 @@\n+hello\n+world\n"; + let req = ApplyGitRequest { + cwd: root.to_path_buf(), + diff: diff.to_string(), + revert: false, + preflight: false, + }; + let r = apply_git_patch(&req).expect("run apply"); + assert_eq!(r.exit_code, 0, "exit code 0"); + // File exists now + assert!(root.join("hello.txt").exists()); + } + + #[test] + fn apply_modify_conflict() { + let _g = env_lock().lock().unwrap(); + let repo = init_repo(); + let root = repo.path(); + // seed file and commit + std::fs::write(root.join("file.txt"), "line1\nline2\nline3\n").unwrap(); + let _ = run(root, &["git", "add", "file.txt"]); + let _ = run(root, &["git", "commit", "-m", "seed"]); + // local edit (unstaged) + std::fs::write(root.join("file.txt"), "line1\nlocal2\nline3\n").unwrap(); + // patch wants to change the same line differently + let diff = "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1,3 +1,3 @@\n line1\n-line2\n+remote2\n line3\n"; + let req = ApplyGitRequest { + cwd: root.to_path_buf(), + diff: diff.to_string(), + revert: false, + preflight: false, + }; + let r = apply_git_patch(&req).expect("run apply"); + assert_ne!(r.exit_code, 0, "non-zero exit on conflict"); + } + + #[test] + fn apply_modify_skipped_missing_index() { + let _g = env_lock().lock().unwrap(); + let repo = init_repo(); + let root = repo.path(); + // Try to modify a file that is not in the index + let diff = "diff --git a/ghost.txt b/ghost.txt\n--- a/ghost.txt\n+++ b/ghost.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n"; + let req = ApplyGitRequest { + cwd: root.to_path_buf(), + diff: diff.to_string(), + revert: false, + preflight: false, + }; + let r = apply_git_patch(&req).expect("run apply"); + assert_ne!(r.exit_code, 0, "non-zero exit on missing index"); + } + + #[test] + fn apply_then_revert_success() { + let _g = env_lock().lock().unwrap(); + let repo = init_repo(); + let root = repo.path(); + // Seed file and commit original content + std::fs::write(root.join("file.txt"), "orig\n").unwrap(); + let _ = run(root, &["git", "add", "file.txt"]); + let _ = run(root, &["git", "commit", "-m", "seed"]); + + // Forward patch: orig -> ORIG + let diff = "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1,1 +1,1 @@\n-orig\n+ORIG\n"; + let apply_req = ApplyGitRequest { + cwd: root.to_path_buf(), + diff: diff.to_string(), + revert: false, + preflight: false, + }; + let res_apply = apply_git_patch(&apply_req).expect("apply ok"); + assert_eq!(res_apply.exit_code, 0, "forward apply succeeded"); + let after_apply = read_file_normalized(&root.join("file.txt")); + assert_eq!(after_apply, "ORIG\n"); + + // Revert patch: ORIG -> orig (stage paths first; engine handles it) + let revert_req = ApplyGitRequest { + cwd: root.to_path_buf(), + diff: diff.to_string(), + revert: true, + preflight: false, + }; + let res_revert = apply_git_patch(&revert_req).expect("revert ok"); + assert_eq!(res_revert.exit_code, 0, "revert apply succeeded"); + let after_revert = read_file_normalized(&root.join("file.txt")); + assert_eq!(after_revert, "orig\n"); + } + + #[test] + fn revert_preflight_does_not_stage_index() { + let _g = env_lock().lock().unwrap(); + let repo = init_repo(); + let root = repo.path(); + // Seed repo and apply forward patch so the working tree reflects the change. + std::fs::write(root.join("file.txt"), "orig\n").unwrap(); + let _ = run(root, &["git", "add", "file.txt"]); + let _ = run(root, &["git", "commit", "-m", "seed"]); + + let diff = "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1,1 +1,1 @@\n-orig\n+ORIG\n"; + let apply_req = ApplyGitRequest { + cwd: root.to_path_buf(), + diff: diff.to_string(), + revert: false, + preflight: false, + }; + let res_apply = apply_git_patch(&apply_req).expect("apply ok"); + assert_eq!(res_apply.exit_code, 0, "forward apply succeeded"); + let (commit_code, _, commit_err) = run(root, &["git", "commit", "-am", "apply change"]); + assert_eq!(commit_code, 0, "commit applied change: {commit_err}"); + + let (_code_before, staged_before, _stderr_before) = + run(root, &["git", "diff", "--cached", "--name-only"]); + + let preflight_req = ApplyGitRequest { + cwd: root.to_path_buf(), + diff: diff.to_string(), + revert: true, + preflight: true, + }; + let res_preflight = apply_git_patch(&preflight_req).expect("preflight ok"); + assert_eq!(res_preflight.exit_code, 0, "revert preflight succeeded"); + let (_code_after, staged_after, _stderr_after) = + run(root, &["git", "diff", "--cached", "--name-only"]); + assert_eq!( + staged_after.trim(), + staged_before.trim(), + "preflight should not stage new paths", + ); + + let after_preflight = read_file_normalized(&root.join("file.txt")); + assert_eq!(after_preflight, "ORIG\n"); + } + + #[test] + fn preflight_blocks_partial_changes() { + let _g = env_lock().lock().unwrap(); + let repo = init_repo(); + let root = repo.path(); + // Build a multi-file diff: one valid add (ok.txt) and one invalid modify (ghost.txt) + let diff = "diff --git a/ok.txt b/ok.txt\nnew file mode 100644\n--- /dev/null\n+++ b/ok.txt\n@@ -0,0 +1,2 @@\n+alpha\n+beta\n\n\ +diff --git a/ghost.txt b/ghost.txt\n--- a/ghost.txt\n+++ b/ghost.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n"; + + // 1) With preflight enabled, nothing should be changed (even though ok.txt could be added) + let req1 = ApplyGitRequest { + cwd: root.to_path_buf(), + diff: diff.to_string(), + revert: false, + preflight: true, + }; + let r1 = apply_git_patch(&req1).expect("preflight apply"); + assert_ne!(r1.exit_code, 0, "preflight reports failure"); + assert!( + !root.join("ok.txt").exists(), + "preflight must prevent adding ok.txt" + ); + assert!( + r1.cmd_for_log.contains("--check"), + "preflight path recorded --check" + ); + + // 2) Without preflight, we should see no --check in the executed command + let req2 = ApplyGitRequest { + cwd: root.to_path_buf(), + diff: diff.to_string(), + revert: false, + preflight: false, + }; + let r2 = apply_git_patch(&req2).expect("direct apply"); + assert_ne!(r2.exit_code, 0, "apply is expected to fail overall"); + assert!( + !r2.cmd_for_log.contains("--check"), + "non-preflight path should not use --check" + ); + } +} diff --git a/codex-rs/mcp-client/src/mcp_client.rs b/codex-rs/mcp-client/src/mcp_client.rs index 087335e66b..27f96494ee 100644 --- a/codex-rs/mcp-client/src/mcp_client.rs +++ b/codex-rs/mcp-client/src/mcp_client.rs @@ -274,8 +274,7 @@ impl McpClient { err.error.code, err.error.message ))), other => Err(anyhow!(format!( - "unexpected message variant received in reply path: {:?}", - other + "unexpected message variant received in reply path: {other:?}" ))), } } diff --git a/codex-rs/protocol-ts/src/lib.rs b/codex-rs/protocol-ts/src/lib.rs index 12c0b7a1aa..848aeb25c7 100644 --- a/codex-rs/protocol-ts/src/lib.rs +++ b/codex-rs/protocol-ts/src/lib.rs @@ -71,7 +71,7 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { .status() .with_context(|| format!("Failed to invoke Prettier at {}", prettier_bin.display()))?; if !status.success() { - return Err(anyhow!("Prettier failed with status {}", status)); + return Err(anyhow!("Prettier failed with status {status}")); } } diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index fd301913e7..ec231437b0 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -7,6 +7,7 @@ use ratatui::layout::Constraint; use ratatui::layout::Layout; use ratatui::layout::Margin; use ratatui::layout::Rect; +use ratatui::style::Style; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; @@ -93,6 +94,7 @@ pub(crate) struct ChatComposer { disable_paste_burst: bool, custom_prompts: Vec, footer_mode: FooterMode, + footer_hint_override: Option>, } /// Popup state – at most one can be visible at any time. @@ -134,6 +136,7 @@ impl ChatComposer { disable_paste_burst: false, custom_prompts: Vec::new(), footer_mode: FooterMode::ShortcutPrompt, + footer_hint_override: None, }; // Apply configuration via the setter to keep side-effects centralized. this.set_disable_paste_burst(disable_paste_burst); @@ -142,7 +145,9 @@ impl ChatComposer { pub fn desired_height(&self, width: u16) -> u16 { let footer_props = self.footer_props(); - let footer_hint_height = footer_height(footer_props); + let footer_hint_height = self + .custom_footer_height() + .unwrap_or_else(|| footer_height(footer_props)); let footer_spacing = Self::footer_spacing(footer_hint_height); let footer_total_height = footer_hint_height + footer_spacing; self.textarea @@ -157,7 +162,9 @@ impl ChatComposer { fn layout_areas(&self, area: Rect) -> [Rect; 3] { let footer_props = self.footer_props(); - let footer_hint_height = footer_height(footer_props); + let footer_hint_height = self + .custom_footer_height() + .unwrap_or_else(|| footer_height(footer_props)); let footer_spacing = Self::footer_spacing(footer_hint_height); let footer_total_height = footer_hint_height + footer_spacing; let popup_constraint = match &self.active_popup { @@ -273,6 +280,12 @@ impl ChatComposer { } } + /// Override the footer hint items displayed beneath the composer. Passing + /// `None` restores the default shortcut footer. + pub(crate) fn set_footer_hint_override(&mut self, items: Option>) { + self.footer_hint_override = items; + } + /// Replace the entire composer content with `text` and reset cursor. pub(crate) fn set_text_content(&mut self, text: String) { // Clear any existing content, placeholders, and attachments first. @@ -1304,6 +1317,12 @@ impl ChatComposer { } } + fn custom_footer_height(&self) -> Option { + self.footer_hint_override + .as_ref() + .map(|items| if items.is_empty() { 0 } else { 1 }) + } + /// Synchronize `self.command_popup` with the current text in the /// textarea. This must be called after every modification that can change /// the text so the popup is shown/updated/hidden as appropriate. @@ -1436,7 +1455,9 @@ impl WidgetRef for ChatComposer { } ActivePopup::None => { let footer_props = self.footer_props(); - let footer_hint_height = footer_height(footer_props); + let custom_height = self.custom_footer_height(); + let footer_hint_height = + custom_height.unwrap_or_else(|| footer_height(footer_props)); let footer_spacing = Self::footer_spacing(footer_hint_height); let hint_rect = if footer_spacing > 0 && footer_hint_height > 0 { let [_, hint_rect] = Layout::vertical([ @@ -1448,7 +1469,27 @@ impl WidgetRef for ChatComposer { } else { popup_rect }; - render_footer(hint_rect, buf, footer_props); + if let Some(items) = self.footer_hint_override.as_ref() { + if !items.is_empty() { + let mut spans = Vec::with_capacity(items.len() * 4); + for (idx, (key, label)) in items.iter().enumerate() { + spans.push(" ".into()); + spans.push(Span::styled(key.clone(), Style::default().bold())); + spans.push(format!(" {label}").into()); + if idx + 1 != items.len() { + spans.push(" ".into()); + } + } + let mut custom_rect = hint_rect; + if custom_rect.width > 2 { + custom_rect.x += 2; + custom_rect.width = custom_rect.width.saturating_sub(2); + } + Line::from(spans).render_ref(custom_rect, buf); + } + } else { + render_footer(hint_rect, buf, footer_props); + } } } let style = user_message_style(terminal_palette::default_bg()); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 5e42e20312..5db6ebe1a7 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -875,7 +875,7 @@ pub(crate) fn new_mcp_tools_output( lines.push(vec![" • Server: ".into(), server.clone().into()].into()); match &cfg.transport { - McpServerTransportConfig::Stdio { command, args, .. } => { + McpServerTransportConfig::Stdio { command, args, env } => { let args_suffix = if args.is_empty() { String::new() } else { @@ -883,6 +883,15 @@ pub(crate) fn new_mcp_tools_output( }; let cmd_display = format!("{command}{args_suffix}"); lines.push(vec![" • Command: ".into(), cmd_display.into()].into()); + + if let Some(env) = env.as_ref() + && !env.is_empty() + { + let mut env_pairs: Vec = + env.iter().map(|(k, v)| format!("{k}={v}")).collect(); + env_pairs.sort(); + lines.push(vec![" • Env: ".into(), env_pairs.join(" ").into()].into()); + } } McpServerTransportConfig::StreamableHttp { url, .. } => { lines.push(vec![" • URL: ".into(), url.clone().into()].into()); diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index dd3dcb9e2c..1d66251ff0 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -55,6 +55,7 @@ mod markdown_render; mod markdown_stream; pub mod onboarding; mod pager_overlay; +pub mod public_widgets; mod render; mod resume_picker; mod session_log; @@ -82,6 +83,9 @@ use crate::onboarding::onboarding_screen::OnboardingScreenArgs; use crate::onboarding::onboarding_screen::run_onboarding_app; use crate::tui::Tui; pub use cli::Cli; +pub use markdown_render::render_markdown_text; +pub use public_widgets::composer_input::ComposerAction; +pub use public_widgets::composer_input::ComposerInput; // (tests access modules directly within the crate) diff --git a/codex-rs/tui/src/markdown_render.rs b/codex-rs/tui/src/markdown_render.rs index cac40cffde..4839346380 100644 --- a/codex-rs/tui/src/markdown_render.rs +++ b/codex-rs/tui/src/markdown_render.rs @@ -33,7 +33,7 @@ impl IndentContext { } #[allow(dead_code)] -pub(crate) fn render_markdown_text(input: &str) -> Text<'static> { +pub fn render_markdown_text(input: &str) -> Text<'static> { let mut options = Options::empty(); options.insert(Options::ENABLE_STRIKETHROUGH); let parser = Parser::new_ext(input, options); diff --git a/codex-rs/tui/src/public_widgets/composer_input.rs b/codex-rs/tui/src/public_widgets/composer_input.rs new file mode 100644 index 0000000000..457e37fe06 --- /dev/null +++ b/codex-rs/tui/src/public_widgets/composer_input.rs @@ -0,0 +1,128 @@ +//! Public wrapper around the internal ChatComposer for simple, reusable text input. +//! +//! This exposes a minimal interface suitable for other crates (e.g., +//! codex-cloud-tasks) to reuse the mature composer behavior: multi-line input, +//! paste heuristics, Enter-to-submit, and Shift+Enter for newline. + +use crossterm::event::KeyEvent; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::widgets::WidgetRef; +use std::time::Duration; + +use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; +use crate::bottom_pane::ChatComposer; +use crate::bottom_pane::InputResult; + +/// Action returned from feeding a key event into the ComposerInput. +pub enum ComposerAction { + /// The user submitted the current text (typically via Enter). Contains the submitted text. + Submitted(String), + /// No submission occurred; UI may need to redraw if `needs_redraw()` returned true. + None, +} + +/// A minimal, public wrapper for the internal `ChatComposer` that behaves as a +/// reusable text input field with submit semantics. +pub struct ComposerInput { + inner: ChatComposer, + _tx: tokio::sync::mpsc::UnboundedSender, + rx: tokio::sync::mpsc::UnboundedReceiver, +} + +impl ComposerInput { + /// Create a new composer input with a neutral placeholder. + pub fn new() -> Self { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + let sender = AppEventSender::new(tx.clone()); + // `enhanced_keys_supported=true` enables Shift+Enter newline hint/behavior. + let inner = ChatComposer::new(true, sender, true, "Compose new task".to_string(), false); + Self { inner, _tx: tx, rx } + } + + /// Returns true if the input is empty. + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Clear the input text. + pub fn clear(&mut self) { + self.inner.set_text_content(String::new()); + } + + /// Feed a key event into the composer and return a high-level action. + pub fn input(&mut self, key: KeyEvent) -> ComposerAction { + let action = match self.inner.handle_key_event(key).0 { + InputResult::Submitted(text) => ComposerAction::Submitted(text), + _ => ComposerAction::None, + }; + self.drain_app_events(); + action + } + + pub fn handle_paste(&mut self, pasted: String) -> bool { + let handled = self.inner.handle_paste(pasted); + self.drain_app_events(); + handled + } + + /// Override the footer hint items displayed under the composer. + /// Each tuple is rendered as "