diff --git a/.github/actions/codex/action.yml b/.github/actions/codex/action.yml index 404194c00f..011cbccfdf 100644 --- a/.github/actions/codex/action.yml +++ b/.github/actions/codex/action.yml @@ -82,20 +82,18 @@ runs: # Note that if we start baking version numbers into the artifact name, # we will need to update this action.yml file to match. - artifact="codex-exec-${triple}.tar.gz" + artifact="codex-${triple}.tar.gz" TAG_ARG="${{ inputs.codex_release_tag }}" # The usage is `gh release download [] [flags]`, so if TAG_ARG # is empty, we do not pass it so we can default to the latest release. gh release download ${TAG_ARG:+$TAG_ARG} --repo openai/codex \ --pattern "$artifact" --output - \ - | tar xzO > /usr/local/bin/codex-exec - chmod +x /usr/local/bin/codex-exec + | tar xzO > /usr/local/bin/codex + chmod +x /usr/local/bin/codex - # Display Codex version to confirm binary integrity; ensure we point it - # at the checked-out repository via --cd so that any subsequent commands - # use the correct working directory. - codex-exec --cd "$GITHUB_WORKSPACE" --version + # Display Codex version to confirm binary integrity. + codex --version - name: Install Bun uses: oven-sh/setup-bun@v2 diff --git a/.github/actions/codex/bun.lock b/.github/actions/codex/bun.lock index 8b546a5ac6..82e12cc4b6 100644 --- a/.github/actions/codex/bun.lock +++ b/.github/actions/codex/bun.lock @@ -11,7 +11,7 @@ "@types/bun": "^1.2.19", "@types/node": "^24.1.0", "prettier": "^3.6.2", - "typescript": "^5.8.3", + "typescript": "^5.9.2", }, }, }, @@ -68,7 +68,7 @@ "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], diff --git a/.github/actions/codex/package.json b/.github/actions/codex/package.json index 21817b8a59..6c7ae9002b 100644 --- a/.github/actions/codex/package.json +++ b/.github/actions/codex/package.json @@ -16,6 +16,6 @@ "@types/bun": "^1.2.19", "@types/node": "^24.1.0", "prettier": "^3.6.2", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } } diff --git a/.github/actions/codex/src/process-label.ts b/.github/actions/codex/src/process-label.ts index 4b4361e118..60207efaaa 100644 --- a/.github/actions/codex/src/process-label.ts +++ b/.github/actions/codex/src/process-label.ts @@ -91,7 +91,38 @@ async function processLabel( labelConfig: LabelConfig, ): Promise { const template = labelConfig.getPromptTemplate(); - const populatedTemplate = await renderPromptTemplate(template, ctx); + + // If this is a review label, prepend explicit PR-diff scoping guidance to + // reduce out-of-scope feedback. Do this before rendering so placeholders in + // the guidance (e.g., {CODEX_ACTION_GITHUB_EVENT_PATH}) are substituted. + const isReview = label.toLowerCase().includes("review"); + const reviewScopeGuidance = ` +PR Diff Scope +- Only review changes between the PR's merge-base and head; do not comment on commits or files outside this range. +- Derive the base/head SHAs from the event JSON at {CODEX_ACTION_GITHUB_EVENT_PATH}, then compute and use the PR diff for all analysis and comments. + +Commands to determine scope +- Resolve SHAs: + - BASE_SHA=$(jq -r '.pull_request.base.sha // .pull_request.base.ref' "{CODEX_ACTION_GITHUB_EVENT_PATH}") + - HEAD_SHA=$(jq -r '.pull_request.head.sha // .pull_request.head.ref' "{CODEX_ACTION_GITHUB_EVENT_PATH}") + - BASE_SHA=$(git rev-parse "$BASE_SHA") + - HEAD_SHA=$(git rev-parse "$HEAD_SHA") +- Prefer triple-dot (merge-base) semantics for PR diffs: + - Changed commits: git log --oneline "$BASE_SHA...$HEAD_SHA" + - Changed files: git diff --name-status "$BASE_SHA...$HEAD_SHA" + - Review hunks: git diff -U0 "$BASE_SHA...$HEAD_SHA" + +Review rules +- Anchor every comment to a file and hunk present in git diff "$BASE_SHA...$HEAD_SHA". +- If you mention context outside the diff, label it as "Follow-up (outside this PR scope)" and keep it brief (<=2 bullets). +- Do not critique commits or files not reachable in the PR range (merge-base(base, head) → head). +`.trim(); + + const effectiveTemplate = isReview + ? `${reviewScopeGuidance}\n\n${template}` + : template; + + const populatedTemplate = await renderPromptTemplate(effectiveTemplate, ctx); // Always run Codex and post the resulting message as a comment. let commentBody = await runCodex(populatedTemplate, ctx); diff --git a/.github/actions/codex/src/run-codex.ts b/.github/actions/codex/src/run-codex.ts index 2c851823e8..3c0255e2ec 100644 --- a/.github/actions/codex/src/run-codex.ts +++ b/.github/actions/codex/src/run-codex.ts @@ -18,7 +18,9 @@ export async function runCodex( const tempDirPath = await mkdtemp(join(tmpdir(), "codex-")); const lastMessageOutput = join(tempDirPath, "codex-prompt.md"); - const args = ["/usr/local/bin/codex-exec"]; + // Use the unified CLI and its `exec` subcommand instead of the old + // standalone `codex-exec` binary. + const args = ["/usr/local/bin/codex", "exec"]; const inputCodexArgs = ctx.tryGet("INPUT_CODEX_ARGS")?.trim(); if (inputCodexArgs) { diff --git a/.github/codex-cli-login.png b/.github/codex-cli-login.png new file mode 100644 index 0000000000..4e59826eaf Binary files /dev/null and b/.github/codex-cli-login.png differ diff --git a/.github/codex-cli-permissions.png b/.github/codex-cli-permissions.png new file mode 100644 index 0000000000..bb48e4a53b Binary files /dev/null and b/.github/codex-cli-permissions.png differ diff --git a/.github/codex-cli-splash.png b/.github/codex-cli-splash.png new file mode 100644 index 0000000000..d3b27d78d0 Binary files /dev/null and b/.github/codex-cli-splash.png differ diff --git a/.github/codex/labels/codex-rust-review.md b/.github/codex/labels/codex-rust-review.md index 2c2893a1fe..ae82d272d7 100644 --- a/.github/codex/labels/codex-rust-review.md +++ b/.github/codex/labels/codex-rust-review.md @@ -6,18 +6,134 @@ Then provide the **review** (1-2 sentences plus bullet points, friendly tone). Things to look out for when doing the review: +## General Principles + - **Make sure the pull request body explains the motivation behind the change.** If the author has failed to do this, call it out, and if you think you can deduce the motivation behind the change, propose copy. - Ideally, the PR body also contains a small summary of the change. For small changes, the PR title may be sufficient. - Each PR should ideally do one conceptual thing. For example, if a PR does a refactoring as well as introducing a new feature, push back and suggest the refactoring be done in a separate PR. This makes things easier for the reviewer, as refactoring changes can often be far-reaching, yet quick to review. -- If the nature of the change seems to have a visual component (which is often the case for changes to `codex-rs/tui`), recommend including a screenshot or video to demonstrate the change, if appropriate. -- Rust files should generally be organized such that the public parts of the API appear near the top of the file and helper functions go below. This is analagous to the "inverted pyramid" structure that is favored in journalism. -- Encourage the use of small enums or the newtype pattern in Rust if it helps readability without adding significant cognitive load or lines of code. -- Be wary of large files and offer suggestions for how to break things into more reasonably-sized files. -- When modifying a `Cargo.toml` file, make sure that dependency lists stay alphabetically sorted. Also consider whether a new dependency is added to the appropriate place (e.g., `[dependencies]` versus `[dev-dependencies]`) -- If you see opportunities for the changes in a diff to use more idiomatic Rust, please make specific recommendations. For example, favor the use of expressions over `return`. - When introducing new code, be on the lookout for code that duplicates existing code. When found, propose a way to refactor the existing code such that it should be reused. + +## Code Organization + - Each create in the Cargo workspace in `codex-rs` has a specific purpose: make a note if you believe new code is not introduced in the correct crate. - When possible, try to keep the `core` crate as small as possible. Non-core but shared logic is often a good candidate for `codex-rs/common`. +- Be wary of large files and offer suggestions for how to break things into more reasonably-sized files. +- Rust files should generally be organized such that the public parts of the API appear near the top of the file and helper functions go below. This is analagous to the "inverted pyramid" structure that is favored in journalism. + +## Assertions in Tests + +Assert the equality of the entire objects instead of doing "piecemeal comparisons," performing `assert_eq!()` on individual fields. + +Note that unit tests also function as "executable documentation." As shown in the following example, "piecemeal comparisons" are often more verbose, provide less coverage, and are not as useful as executable documentation. + +For example, suppose you have the following enum: + +```rust +#[derive(Debug, PartialEq)] +enum Message { + Request { + id: String, + method: String, + params: Option, + }, + Notification { + method: String, + params: Option, + }, +} +``` + +This is an example of a _piecemeal_ comparison: + +```rust +// BAD: Piecemeal Comparison + +#[test] +fn test_get_latest_messages() { + let messages = get_latest_messages(); + assert_eq!(messages.len(), 2); + + let m0 = &messages[0]; + match m0 { + Message::Request { id, method, params } => { + assert_eq!(id, "123"); + assert_eq!(method, "subscribe"); + assert_eq!( + *params, + Some(json!({ + "conversation_id": "x42z86" + })) + ) + } + Message::Notification { .. } => { + panic!("expected Request"); + } + } + + let m1 = &messages[1]; + match m1 { + Message::Request { .. } => { + panic!("expected Notification"); + } + Message::Notification { method, params } => { + assert_eq!(method, "log"); + assert_eq!( + *params, + Some(json!({ + "level": "info", + "message": "subscribed" + })) + ) + } + } +} +``` + +This is a _deep_ comparison: + +```rust +// GOOD: Verify the entire structure with a single assert_eq!(). + +use pretty_assertions::assert_eq; + +#[test] +fn test_get_latest_messages() { + let messages = get_latest_messages(); + + assert_eq!( + vec![ + Message::Request { + id: "123".to_string(), + method: "subscribe".to_string(), + params: Some(json!({ + "conversation_id": "x42z86" + })), + }, + Message::Notification { + method: "log".to_string(), + params: Some(json!({ + "level": "info", + "message": "subscribed" + })), + }, + ], + messages, + ); +} +``` + +## More Tactical Rust Things To Look Out For + +- Do not use `unsafe` (unless you have a really, really good reason like using an operating system API directly and no safe wrapper exists). For example, there are cases where it is tempting to use `unsafe` in order to use `std::env::set_var()`, but this indeed `unsafe` and has led to race conditions on multiple occasions. (When this happens, find a mechanism other than environment variables to use for configuration.) +- Encourage the use of small enums or the newtype pattern in Rust if it helps readability without adding significant cognitive load or lines of code. +- If you see opportunities for the changes in a diff to use more idiomatic Rust, please make specific recommendations. For example, favor the use of expressions over `return`. +- When modifying a `Cargo.toml` file, make sure that dependency lists stay alphabetically sorted. Also consider whether a new dependency is added to the appropriate place (e.g., `[dependencies]` versus `[dev-dependencies]`) + +## Pull Request Body + +- If the nature of the change seems to have a visual component (which is often the case for changes to `codex-rs/tui`), recommend including a screenshot or video to demonstrate the change, if appropriate. - References to existing GitHub issues and PRs are encouraged, where appropriate, though you likely do not have network access, so may not be able to help here. +# PR Information + {CODEX_ACTION_GITHUB_EVENT_PATH} contains the JSON that triggered this GitHub workflow. It contains the `base` and `head` refs that define this PR. Both refs are available locally. diff --git a/.github/dotslash-config.json b/.github/dotslash-config.json index 1e32001e66..82b9eb93b3 100644 --- a/.github/dotslash-config.json +++ b/.github/dotslash-config.json @@ -1,27 +1,23 @@ { "outputs": { - "codex-exec": { - "platforms": { - "macos-aarch64": { "regex": "^codex-exec-aarch64-apple-darwin\\.zst$", "path": "codex-exec" }, - "macos-x86_64": { "regex": "^codex-exec-x86_64-apple-darwin\\.zst$", "path": "codex-exec" }, - "linux-x86_64": { "regex": "^codex-exec-x86_64-unknown-linux-musl\\.zst$", "path": "codex-exec" }, - "linux-aarch64": { "regex": "^codex-exec-aarch64-unknown-linux-musl\\.zst$", "path": "codex-exec" } - } - }, - "codex": { "platforms": { - "macos-aarch64": { "regex": "^codex-aarch64-apple-darwin\\.zst$", "path": "codex" }, - "macos-x86_64": { "regex": "^codex-x86_64-apple-darwin\\.zst$", "path": "codex" }, - "linux-x86_64": { "regex": "^codex-x86_64-unknown-linux-musl\\.zst$", "path": "codex" }, - "linux-aarch64": { "regex": "^codex-aarch64-unknown-linux-musl\\.zst$", "path": "codex" } - } - }, - - "codex-linux-sandbox": { - "platforms": { - "linux-x86_64": { "regex": "^codex-linux-sandbox-x86_64-unknown-linux-musl\\.zst$", "path": "codex-linux-sandbox" }, - "linux-aarch64": { "regex": "^codex-linux-sandbox-aarch64-unknown-linux-musl\\.zst$", "path": "codex-linux-sandbox" } + "macos-aarch64": { + "regex": "^codex-aarch64-apple-darwin\\.zst$", + "path": "codex" + }, + "macos-x86_64": { + "regex": "^codex-x86_64-apple-darwin\\.zst$", + "path": "codex" + }, + "linux-x86_64": { + "regex": "^codex-x86_64-unknown-linux-musl\\.zst$", + "path": "codex" + }, + "linux-aarch64": { + "regex": "^codex-aarch64-unknown-linux-musl\\.zst$", + "path": "codex" + } } } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d8675fa5f..32fb070924 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,35 +44,10 @@ jobs: # Run all tasks using workspace filters - - name: Check TypeScript code formatting - working-directory: codex-cli - run: pnpm run format - - - name: Check Markdown and config file formatting - run: pnpm run format - - - name: Run tests - run: pnpm run test - - - name: Lint - run: | - pnpm --filter @openai/codex exec -- eslint src tests --ext ts --ext tsx \ - --report-unused-disable-directives \ - --rule "no-console:error" \ - --rule "no-debugger:error" \ - --max-warnings=-1 - - - name: Type-check - run: pnpm run typecheck - - - name: Build - run: pnpm run build - - name: Ensure staging a release works. - working-directory: codex-cli env: GH_TOKEN: ${{ github.token }} - run: pnpm stage-release + run: ./codex-cli/scripts/stage_release.sh - name: Ensure root README.md contains only ASCII and certain Unicode code points run: ./scripts/asciicheck.py README.md diff --git a/.github/workflows/codex.yml b/.github/workflows/codex.yml index 18fe74cc85..6ca0d57f46 100644 --- a/.github/workflows/codex.yml +++ b/.github/workflows/codex.yml @@ -39,37 +39,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - # We install the dependencies like we would for an ordinary CI job, - # particularly because Codex will not have network access to install - # these dependencies. - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10.8.1 - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "store_path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ steps.pnpm-cache.outputs.store_path }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install dependencies - run: pnpm install - - uses: dtolnay/rust-toolchain@1.88 with: targets: x86_64-unknown-linux-gnu diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 3f1c084d91..8bf7361da9 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -70,6 +70,8 @@ jobs: target: aarch64-unknown-linux-musl - runner: ubuntu-24.04-arm target: aarch64-unknown-linux-gnu + - runner: windows-latest + target: x86_64-pc-windows-msvc steps: - uses: actions/checkout@v4 @@ -93,7 +95,7 @@ jobs: sudo apt install -y musl-tools pkg-config - name: Cargo build - run: cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-exec --bin codex-linux-sandbox + run: cargo build --target ${{ matrix.target }} --release --bin codex - name: Stage artifacts shell: bash @@ -101,18 +103,11 @@ jobs: dest="dist/${{ matrix.target }}" mkdir -p "$dest" - cp target/${{ matrix.target }}/release/codex-exec "$dest/codex-exec-${{ matrix.target }}" - cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" - - # After https://github.com/openai/codex/pull/1228 is merged and a new - # release is cut with an artifacts built after that PR, the `-gnu` - # variants can go away as we will only use the `-musl` variants. - - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'aarch64-unknown-linux-musl' }} - name: Stage Linux-only artifacts - shell: bash - run: | - dest="dist/${{ matrix.target }}" - cp target/${{ matrix.target }}/release/codex-linux-sandbox "$dest/codex-linux-sandbox-${{ matrix.target }}" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + else + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" + fi - name: Compress artifacts shell: bash @@ -126,7 +121,6 @@ jobs: # we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) - # ...same naming for codex-exec-* and codex-linux-sandbox-* # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. @@ -160,6 +154,9 @@ jobs: runs-on: ubuntu-latest steps: + - name: Checkout repository + uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 with: path: dist @@ -175,15 +172,31 @@ jobs: version="${GITHUB_REF_NAME#rust-v}" echo "name=${version}" >> $GITHUB_OUTPUT + - name: Stage npm package + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + TMP_DIR="${RUNNER_TEMP}/npm-stage" + python3 codex-cli/scripts/stage_rust_release.py \ + --release-version "${{ steps.release_name.outputs.name }}" \ + --tmp "${TMP_DIR}" + mkdir -p dist/npm + # Produce an npm-ready tarball using `npm pack` and store it in dist/npm. + # We then rename it to a stable name used by our publishing script. + (cd "$TMP_DIR" && npm pack --pack-destination "${GITHUB_WORKSPACE}/dist/npm") + mv "${GITHUB_WORKSPACE}"/dist/npm/*.tgz \ + "${GITHUB_WORKSPACE}/dist/npm/codex-npm-${{ steps.release_name.outputs.name }}.tgz" + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} files: dist/** - # For now, tag releases as "prerelease" because we are not claiming - # the Rust CLI is stable yet. - prerelease: true + # Mark as prerelease only when the version has a suffix after x.y.z + # (e.g. -alpha, -beta). Otherwise publish a normal release. + prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - uses: facebook/dotslash-publish-release@v2 env: diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index e02c24e2b5..0000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -pnpm lint-staged \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 1712f5989b..aadeca0867 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,8 @@ "editor.defaultFormatter": "tamasfe.even-better-toml", "editor.formatOnSave": true, }, - "evenBetterToml.formatter.reorderArrays": true, + // Array order for options in ~/.codex/config.toml such as `notify` and the + // `args` for an MCP server is significant, so we disable reordering. + "evenBetterToml.formatter.reorderArrays": false, "evenBetterToml.formatter.reorderKeys": true, } diff --git a/AGENTS.md b/AGENTS.md index 27af48ae60..5c3f659c35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,9 @@ In the codex-rs folder where the rust code lives: -- Never add or modify any code related to `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR`. You operate in a sandbox where `CODEX_SANDBOX_NETWORK_DISABLED=1` will be set whenever you use the `shell` tool. Any existing code that uses `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` was authored with this fact in mind. It is often used to early exit out of tests that the author knew you would not be able to run given your sandbox limitations. +- Never add or modify any code related to `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` or `CODEX_SANDBOX_ENV_VAR`. + - You operate in a sandbox where `CODEX_SANDBOX_NETWORK_DISABLED=1` will be set whenever you use the `shell` tool. Any existing code that uses `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` was authored with this fact in mind. It is often used to early exit out of tests that the author knew you would not be able to run given your sandbox limitations. + - Similarly, when you spawn a process using Seatbelt (`/usr/bin/sandbox-exec`), `CODEX_SANDBOX=seatbelt` will be set on the child process. Integration tests that want to run Seatbelt themselves cannot be run under Seatbelt, so checks for `CODEX_SANDBOX=seatbelt` are also often used to early exit out of tests, as appropriate. Before creating a pull request with changes to `codex-rs`, run `just fmt` (in `codex-rs` directory) to format the code and `just fix` (in `codex-rs` directory) to fix any linter issues in the code, ensure the test suite passes by running `cargo test --all-features` in the `codex-rs` directory. diff --git a/README.md b/README.md index c7f6a1d595..89063b7e7b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@

OpenAI Codex CLI

-

Lightweight coding agent that runs in your terminal

npm i -g @openai/codex
or brew install codex

-This is the home of the **Codex CLI**, which is a coding agent from OpenAI that runs locally on your computer. If you are looking for the _cloud-based agent_ from OpenAI, **Codex [Web]**, see . +

Codex CLI is a coding agent from OpenAI that runs locally on your computer.
If you are looking for the cloud-based agent from OpenAI, Codex Web, see chatgpt.com/codex.

- +

+ Codex CLI splash +

--- @@ -14,21 +15,28 @@ This is the home of the **Codex CLI**, which is a coding agent from OpenAI that -- [Experimental technology disclaimer](#experimental-technology-disclaimer) - [Quickstart](#quickstart) - - [OpenAI API Users](#openai-api-users) - - [OpenAI Plus/Pro Users](#openai-pluspro-users) -- [Why Codex?](#why-codex) -- [Security model & permissions](#security-model--permissions) + - [Installing and running Codex CLI](#installing-and-running-codex-cli) + - [Using Codex with your ChatGPT plan](#using-codex-with-your-chatgpt-plan) + - [Connecting through VPS or remote](#connecting-through-vps-or-remote) + - [Usage-based billing alternative: Use an OpenAI API key](#usage-based-billing-alternative-use-an-openai-api-key) + - [Choosing Codex's level of autonomy](#choosing-codexs-level-of-autonomy) + - [**1. Read/write**](#1-readwrite) + - [**2. Read-only**](#2-read-only) + - [**3. Advanced configuration**](#3-advanced-configuration) + - [Can I run without ANY approvals?](#can-i-run-without-any-approvals) + - [Fine-tuning in `config.toml`](#fine-tuning-in-configtoml) + - [Example prompts](#example-prompts) +- [Running with a prompt as input](#running-with-a-prompt-as-input) +- [Using Open Source Models](#using-open-source-models) - [Platform sandboxing details](#platform-sandboxing-details) +- [Experimental technology disclaimer](#experimental-technology-disclaimer) - [System requirements](#system-requirements) - [CLI reference](#cli-reference) - [Memory & project docs](#memory--project-docs) - [Non-interactive / CI mode](#non-interactive--ci-mode) - [Model Context Protocol (MCP)](#model-context-protocol-mcp) - [Tracing / verbose logging](#tracing--verbose-logging) -- [Recipes](#recipes) -- [Installation](#installation) - [DotSlash](#dotslash) - [Configuration](#configuration) - [FAQ](#faq) @@ -53,55 +61,170 @@ This is the home of the **Codex CLI**, which is a coding agent from OpenAI that --- -## Experimental technology disclaimer - -Codex CLI is an experimental project under active development. It is not yet stable, may contain bugs, incomplete features, or undergo breaking changes. We're building it in the open with the community and welcome: - -- Bug reports -- Feature requests -- Pull requests -- Good vibes - -Help us improve by filing issues or submitting PRs (see the section below for how to contribute)! - ## Quickstart +### Installing and running Codex CLI + Install globally with your preferred package manager: ```shell npm install -g @openai/codex # Alternatively: `brew install codex` ``` -Or go to the [latest GitHub Release](https://github.com/openai/codex/releases/latest) and download the appropriate binary for your platform. +Then simply run `codex` to get started: -### OpenAI API Users +```shell +codex +``` -Next, set your OpenAI API key as an environment variable: +
+You can also go to the latest GitHub Release and download the appropriate binary for your platform. + +Each GitHub Release contains many executables, but in practice, you likely want one of these: + +- macOS + - Apple Silicon/arm64: `codex-aarch64-apple-darwin.tar.gz` + - x86_64 (older Mac hardware): `codex-x86_64-apple-darwin.tar.gz` +- Linux + - x86_64: `codex-x86_64-unknown-linux-musl.tar.gz` + - arm64: `codex-aarch64-unknown-linux-musl.tar.gz` + +Each archive contains a single entry with the platform baked into the name (e.g., `codex-x86_64-unknown-linux-musl`), so you likely want to rename it to `codex` after extracting it. + +
+ +### Using Codex with your ChatGPT plan + +

+ Codex CLI login +

+ +After you run `codex` select Sign in with ChatGPT. You'll need a Plus, Pro, or Team ChatGPT account, and will get access to our latest models, including `gpt-5`, at no extra cost to your plan. (Enterprise is coming soon.) + +> Important: If you've used the Codex CLI before, you'll need to follow these steps to migrate from usage-based billing with your API key: +> +> 1. Update the CLI with `codex update` and ensure `codex --version` is greater than 0.13 +> 2. Ensure that there is no `OPENAI_API_KEY` environment variable set. (Check that `env | grep 'OPENAI_API_KEY'` returns empty) +> 3. Run `codex login` again + +If you encounter problems with the login flow, please comment on [this issue](https://github.com/openai/codex/issues/1243). + +### Connecting through VPS or remote + +If you run Codex on a remote machine (VPS/server) without a local browser, the login helper starts a server on `localhost:1455` on the remote host. To complete login in your local browser, forward that port to your machine before starting the login flow: + +```bash +# From your local machine +ssh -L 1455:localhost:1455 @ +``` + +Then, in that SSH session, run `codex` and select "Sign in with ChatGPT". When prompted, open the printed URL (it will be `http://localhost:1455/...`) in your local browser. The traffic will be tunneled to the remote server. + +### Usage-based billing alternative: Use an OpenAI API key + +If you prefer to pay-as-you-go, you can still authenticate with your OpenAI API key by setting it as an environment variable: ```shell export OPENAI_API_KEY="your-api-key-here" ``` -> [!NOTE] -> This command sets the key only for your current terminal session. You can add the `export` line to your shell's configuration file (e.g., `~/.zshrc`), but we recommend setting it for the session. +Notes: -### OpenAI Plus/Pro Users +- This command only sets the key for your current terminal session, which we recommend. To set it for all future sessions, you can also add the `export` line to your shell's configuration file (e.g., `~/.zshrc`). +- If you have signed in with ChatGPT, Codex will default to using your ChatGPT credits. If you wish to use your API key, use the `/logout` command to clear your ChatGPT authentication. -If you have a paid OpenAI account, run the following to start the login process: +### Choosing Codex's level of autonomy -``` -codex login +We always recommend running Codex in its default sandbox that gives you strong guardrails around what the agent can do. The default sandbox prevents it from editing files outside its workspace, or from accessing the network. + +When you launch Codex in a new folder, it detects whether the folder is version controlled and recommends one of two levels of autonomy: + +#### **1. Read/write** + +- Codex can run commands and write files in the workspace without approval. +- To write files in other folders, access network, update git or perform other actions protected by the sandbox, Codex will need your permission. +- By default, the workspace includes the current directory, as well as temporary directories like `/tmp`. You can see what directories are in the workspace with the `/status` command. See the docs for how to customize this behavior. +- Advanced: You can manually specify this configuration by running `codex --sandbox workspace-write --ask-for-approval on-request` +- This is the recommended default for version-controlled folders. + +#### **2. Read-only** + +- Codex can run read-only commands without approval. +- To edit files, access network, or perform other actions protected by the sandbox, Codex will need your permission. +- Advanced: You can manually specify this configuration by running `codex --sandbox read-only --ask-for-approval on-request` +- This is the recommended default non-version-controlled folders. + +#### **3. Advanced configuration** + +Codex gives you fine-grained control over the sandbox with the `--sandbox` option, and over when it requests approval with the `--ask-for-approval` option. Run `codex help` for more on these options. + +#### Can I run without ANY approvals? + +Yes, run codex non-interactively with `--ask-for-approval never`. This option works with all `--sandbox` options, so you still have full control over Codex's level of autonomy. It will make its best attempt with whatever contrainsts you provide. For example: + +- Use `codex --ask-for-approval never --sandbox read-only` when you are running many agents to answer questions in parallel in the same workspace. +- Use `codex --ask-for-approval never --sandbox workspace-write` when you want the agent to non-interactively take time to produce the best outcome, with strong guardrails around its behavior. +- Use `codex --ask-for-approval never --sandbox danger-full-access` to dangerously give the agent full autonomy. Because this disables important safety mechanisms, we recommend against using this unless running Codex in an isolated environment. + +#### Fine-tuning in `config.toml` + +```toml +# approval mode +approval_policy = "untrusted" +sandbox_mode = "read-only" + +# full-auto mode +approval_policy = "on-request" +sandbox_mode = "workspace-write" + +# Optional: allow network in workspace-write mode +[sandbox_workspace_write] +network_access = true ``` -If you complete the process successfully, you should have a `~/.codex/auth.json` file that contains the credentials that Codex will use. +You can also save presets as **profiles**: -To verify whether you are currently logged in, run: +```toml +[profiles.full_auto] +approval_policy = "on-request" +sandbox_mode = "workspace-write" -``` -codex login status +[profiles.readonly_quiet] +approval_policy = "never" +sandbox_mode = "read-only" ``` -If you encounter problems with the login flow, please comment on . +### Example prompts + +Below are a few bite-size examples you can copy-paste. Replace the text in quotes with your own task. See the [prompting guide](https://github.com/openai/codex/blob/main/codex-cli/examples/prompting_guide.md) for more tips and usage patterns. + +| ✨ | What you type | What happens | +| --- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| 1 | `codex "Refactor the Dashboard component to React Hooks"` | Codex rewrites the class component, runs `npm test`, and shows the diff. | +| 2 | `codex "Generate SQL migrations for adding a users table"` | Infers your ORM, creates migration files, and runs them in a sandboxed DB. | +| 3 | `codex "Write unit tests for utils/date.ts"` | Generates tests, executes them, and iterates until they pass. | +| 4 | `codex "Bulk-rename *.jpeg -> *.jpg with git mv"` | Safely renames files and updates imports/usages. | +| 5 | `codex "Explain what this regex does: ^(?=.*[A-Z]).{8,}$"` | Outputs a step-by-step human explanation. | +| 6 | `codex "Carefully review this repo, and propose 3 high impact well-scoped PRs"` | Suggests impactful PRs in the current codebase. | +| 7 | `codex "Look for vulnerabilities and create a security review report"` | Finds and explains security bugs. | + +## Running with a prompt as input + +You can also run Codex CLI with a prompt as input: + +```shell +codex "explain this codebase to me" +``` + +```shell +codex --full-auto "create the fanciest todo-list app" +``` + +That's it - Codex will scaffold a file, run it inside a sandbox, install any +missing dependencies, and show you the live result. Approve the changes and +they'll be committed to your working directory. + +## Using Open Source Models
Use --profile to use other models @@ -162,68 +285,40 @@ model = "mistral" This way, you can specify one command-line argument (.e.g., `--profile o3`, `--profile mistral`) to override multiple settings together.
-
-Run interactively: +Codex can run fully locally against an OpenAI-compatible OSS host (like Ollama) using the `--oss` flag: -```shell -codex +- Interactive UI: + - codex --oss +- Non-interactive (programmatic) mode: + - echo "Refactor utils" | codex exec --oss + +Model selection when using `--oss`: + +- If you omit `-m/--model`, Codex defaults to -m gpt-oss:20b and will verify it exists locally (downloading if needed). +- To pick a different size, pass one of: + - -m "gpt-oss:20b" + - -m "gpt-oss:120b" + +Point Codex at your own OSS host: + +- By default, `--oss` talks to http://localhost:11434/v1. +- To use a different host, set one of these environment variables before running Codex: + - CODEX_OSS_BASE_URL, for example: + - CODEX_OSS_BASE_URL="http://my-ollama.example.com:11434/v1" codex --oss -m gpt-oss:20b + - or CODEX_OSS_PORT (when the host is localhost): + - CODEX_OSS_PORT=11434 codex --oss + +Advanced: you can persist this in your config instead of environment variables by overriding the built-in `oss` provider in `~/.codex/config.toml`: + +```toml +[model_providers.oss] +name = "Open Source" +base_url = "http://my-ollama.example.com:11434/v1" ``` -Or, run with a prompt as input (and optionally in `Full Auto` mode): - -```shell -codex "explain this codebase to me" -``` - -```shell -codex --full-auto "create the fanciest todo-list app" -``` - -That's it - Codex will scaffold a file, run it inside a sandbox, install any -missing dependencies, and show you the live result. Approve the changes and -they'll be committed to your working directory. - --- -## Why Codex? - -Codex CLI is built for developers who already **live in the terminal** and want -ChatGPT-level reasoning **plus** the power to actually run code, manipulate -files, and iterate - all under version control. In short, it's _chat-driven -development_ that understands and executes your repo. - -- **Zero setup** - bring your OpenAI API key and it just works! -- **Full auto-approval, while safe + secure** by running network-disabled and directory-sandboxed -- **Multimodal** - pass in screenshots or diagrams to implement features ✨ - -And it's **fully open-source** so you can see and contribute to how it develops! - ---- - -## Security model & permissions - -Codex lets you decide _how much autonomy_ you want to grant the agent. The following options can be configured independently: - -- [`approval_policy`](./codex-rs/config.md#approval_policy) determines when you should be prompted to approve whether Codex can execute a command -- [`sandbox`](./codex-rs/config.md#sandbox) determines the _sandbox policy_ that Codex uses to execute untrusted commands - -By default, Codex runs with `--ask-for-approval untrusted` and `--sandbox read-only`, which means that: - -- The user is prompted to approve every command not on the set of "trusted" commands built into Codex (`cat`, `ls`, etc.) -- Approved commands are run outside of a sandbox because user approval implies "trust," in this case. - -Running Codex with the `--full-auto` convenience flag changes the configuration to `--ask-for-approval on-failure` and `--sandbox workspace-write`, which means that: - -- Codex does not initially ask for user approval before running an individual command. -- Though when it runs a command, it is run under a sandbox in which: - - It can read any file on the system. - - It can only write files under the current directory (or the directory specified via `--cd`). - - Network requests are completely disabled. -- Only if the command exits with a non-zero exit code will it ask the user for approval. If granted, it will re-attempt the command outside of the sandbox. (A common case is when Codex cannot `npm install` a dependency because that requires network access.) - -Again, these two options can be configured independently. For example, if you want Codex to perform an "exploration" where you are happy for it to read anything it wants but you never want to be prompted, you could run Codex with `--ask-for-approval never` and `--sandbox read-only`. - ### Platform sandboxing details The mechanism Codex uses to implement the sandbox policy depends on your OS: @@ -235,6 +330,19 @@ Note that when running Linux in a containerized environment such as Docker, sand --- +## Experimental technology disclaimer + +Codex CLI is an experimental project under active development. It is not yet stable, may contain bugs, incomplete features, or undergo breaking changes. We're building it in the open with the community and welcome: + +- Bug reports +- Feature requests +- Pull requests +- Good vibes + +Help us improve by filing issues or submitting PRs (see the section below for how to contribute)! + +--- + ## System requirements | Requirement | Details | @@ -310,52 +418,6 @@ See the Rust documentation on [`RUST_LOG`](https://docs.rs/env_logger/latest/env --- -## Recipes - -Below are a few bite-size examples you can copy-paste. Replace the text in quotes with your own task. See the [prompting guide](https://github.com/openai/codex/blob/main/codex-cli/examples/prompting_guide.md) for more tips and usage patterns. - -| ✨ | What you type | What happens | -| --- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| 1 | `codex "Refactor the Dashboard component to React Hooks"` | Codex rewrites the class component, runs `npm test`, and shows the diff. | -| 2 | `codex "Generate SQL migrations for adding a users table"` | Infers your ORM, creates migration files, and runs them in a sandboxed DB. | -| 3 | `codex "Write unit tests for utils/date.ts"` | Generates tests, executes them, and iterates until they pass. | -| 4 | `codex "Bulk-rename *.jpeg -> *.jpg with git mv"` | Safely renames files and updates imports/usages. | -| 5 | `codex "Explain what this regex does: ^(?=.*[A-Z]).{8,}$"` | Outputs a step-by-step human explanation. | -| 6 | `codex "Carefully review this repo, and propose 3 high impact well-scoped PRs"` | Suggests impactful PRs in the current codebase. | -| 7 | `codex "Look for vulnerabilities and create a security review report"` | Finds and explains security bugs. | - ---- - -## Installation - -
-Install Codex CLI using your preferred package manager. - -From `brew` (recommended, downloads only the binary for your platform): - -```bash -brew install codex -``` - -From `npm` (generally more readily available, but downloads binaries for all supported platforms): - -```bash -npm i -g @openai/codex -``` - -Or go to the [latest GitHub Release](https://github.com/openai/codex/releases/latest) and download the appropriate binary for your platform. - -Admittedly, each GitHub Release contains many executables, but in practice, you likely want one of these: - -- macOS - - Apple Silicon/arm64: `codex-aarch64-apple-darwin.tar.gz` - - x86_64 (older Mac hardware): `codex-x86_64-apple-darwin.tar.gz` -- Linux - - x86_64: `codex-x86_64-unknown-linux-musl.tar.gz` - - arm64: `codex-aarch64-unknown-linux-musl.tar.gz` - -Each archive contains a single entry with the platform baked into the name (e.g., `codex-x86_64-unknown-linux-musl`), so you likely want to rename it to `codex` after extracting it. - ### DotSlash The GitHub Release also contains a [DotSlash](https://dotslash-cli.com/) file for the Codex CLI named `codex`. Using a DotSlash file makes it possible to make a lightweight commit to source control to ensure all contributors use the same version of an executable, regardless of what platform they use for development. diff --git a/codex-cli/.editorconfig b/codex-cli/.editorconfig deleted file mode 100644 index f55d420563..0000000000 --- a/codex-cli/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 - -[*.{js,ts,jsx,tsx}] -indent_style = space -indent_size = 2 \ No newline at end of file diff --git a/codex-cli/.eslintrc.cjs b/codex-cli/.eslintrc.cjs deleted file mode 100644 index a623d2edb0..0000000000 --- a/codex-cli/.eslintrc.cjs +++ /dev/null @@ -1,107 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, node: true, es2020: true }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - ], - ignorePatterns: [ - ".eslintrc.cjs", - "build.mjs", - "dist", - "vite.config.ts", - "src/components/vendor", - ], - parser: "@typescript-eslint/parser", - parserOptions: { - tsconfigRootDir: __dirname, - project: ["./tsconfig.json"], - }, - plugins: ["import", "react-hooks", "react-refresh"], - rules: { - // Imports - "@typescript-eslint/consistent-type-imports": "error", - "import/no-cycle": ["error", { maxDepth: 1 }], - "import/no-duplicates": "error", - "import/order": [ - "error", - { - groups: ["type"], - "newlines-between": "always", - alphabetize: { - order: "asc", - caseInsensitive: false, - }, - }, - ], - // We use the import/ plugin instead. - "sort-imports": "off", - - "@typescript-eslint/array-type": ["error", { default: "generic" }], - // FIXME(mbolin): Introduce this. - // "@typescript-eslint/explicit-function-return-type": "error", - "@typescript-eslint/explicit-module-boundary-types": "error", - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/switch-exhaustiveness-check": [ - "error", - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: true, - }, - ], - - // Use typescript-eslint/no-unused-vars, no-unused-vars reports - // false positives with typescript - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - - curly: "error", - - eqeqeq: ["error", "always", { null: "never" }], - "react-refresh/only-export-components": [ - "error", - { allowConstantExport: true }, - ], - "no-await-in-loop": "error", - "no-bitwise": "error", - "no-caller": "error", - // This is fine during development, but should not be checked in. - "no-console": "error", - // This is fine during development, but should not be checked in. - "no-debugger": "error", - "no-duplicate-case": "error", - "no-eval": "error", - "no-ex-assign": "error", - "no-return-await": "error", - "no-param-reassign": "error", - "no-script-url": "error", - "no-self-compare": "error", - "no-unsafe-finally": "error", - "no-var": "error", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "error", - }, - overrides: [ - { - // apply only to files under tests/ - files: ["tests/**/*.{ts,tsx,js,jsx}"], - rules: { - "@typescript-eslint/no-explicit-any": "off", - "import/order": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-var-requires": "off", - "no-await-in-loop": "off", - "no-control-regex": "off", - }, - }, - ], -}; diff --git a/codex-cli/HUSKY.md b/codex-cli/HUSKY.md deleted file mode 100644 index d525e2f743..0000000000 --- a/codex-cli/HUSKY.md +++ /dev/null @@ -1,45 +0,0 @@ -# Husky Git Hooks - -This project uses [Husky](https://typicode.github.io/husky/) to enforce code quality checks before commits and pushes. - -## What's Included - -- **Pre-commit Hook**: Runs lint-staged to check files that are about to be committed. - - - Lints and formats TypeScript/TSX files using ESLint and Prettier - - Formats JSON, MD, and YML files using Prettier - -- **Pre-push Hook**: Runs tests and type checking before pushing to the remote repository. - - Executes `npm test` to run all tests - - Executes `npm run typecheck` to check TypeScript types - -## Benefits - -- Ensures consistent code style across the project -- Prevents pushing code with failing tests or type errors -- Reduces the need for style-related code review comments -- Improves overall code quality - -## For Contributors - -You don't need to do anything special to use these hooks. They will automatically run when you commit or push code. - -If you need to bypass the hooks in exceptional cases: - -```bash -# Skip pre-commit hooks -git commit -m "Your message" --no-verify - -# Skip pre-push hooks -git push --no-verify -``` - -Note: Please use these bypass options sparingly and only when absolutely necessary. - -## Troubleshooting - -If you encounter any issues with the hooks: - -1. Make sure you have the latest dependencies installed: `npm install` -2. Ensure the hook scripts are executable (Unix systems): `chmod +x .husky/pre-commit .husky/pre-push` -3. Check if there are any ESLint or Prettier configuration issues in your code diff --git a/codex-cli/bin/codex.js b/codex-cli/bin/codex.js index ae1fb9593c..d92d8f2f4f 100755 --- a/codex-cli/bin/codex.js +++ b/codex-cli/bin/codex.js @@ -1,153 +1,123 @@ #!/usr/bin/env node // Unified entry point for the Codex CLI. -/* - * Behavior - * ========= - * 1. By default we import the JavaScript implementation located in - * dist/cli.js. - * - * 2. Developers can opt-in to a pre-compiled Rust binary by setting the - * environment variable CODEX_RUST to a truthy value (`1`, `true`, etc.). - * When that variable is present we resolve the correct binary for the - * current platform / architecture and execute it via child_process. - * - * If the CODEX_RUST=1 is specified and there is no native binary for the - * current platform / architecture, an error is thrown. - */ -import fs from "fs"; import path from "path"; -import { fileURLToPath, pathToFileURL } from "url"; - -// Determine whether the user explicitly wants the Rust CLI. +import { fileURLToPath } from "url"; // __dirname equivalent in ESM const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// For the @native release of the Node module, the `use-native` file is added, -// indicating we should default to the native binary. For other releases, -// setting CODEX_RUST=1 will opt-in to the native binary, if included. -const wantsNative = fs.existsSync(path.join(__dirname, "use-native")) || - (process.env.CODEX_RUST != null - ? ["1", "true", "yes"].includes(process.env.CODEX_RUST.toLowerCase()) - : false); +const { platform, arch } = process; -// Try native binary if requested. -if (wantsNative && process.platform !== 'win32') { - const { platform, arch } = process; - - let targetTriple = null; - switch (platform) { - case "linux": - case "android": - switch (arch) { - case "x64": - targetTriple = "x86_64-unknown-linux-musl"; - break; - case "arm64": - targetTriple = "aarch64-unknown-linux-musl"; - break; - default: - break; - } - break; - case "darwin": - switch (arch) { - case "x64": - targetTriple = "x86_64-apple-darwin"; - break; - case "arm64": - targetTriple = "aarch64-apple-darwin"; - break; - default: - break; - } - break; - default: - break; - } - - if (!targetTriple) { - throw new Error(`Unsupported platform: ${platform} (${arch})`); - } - - const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`); - - // Use an asynchronous spawn instead of spawnSync so that Node is able to - // respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is - // executing. This allows us to forward those signals to the child process - // and guarantees that when either the child terminates or the parent - // receives a fatal signal, both processes exit in a predictable manner. - const { spawn } = await import("child_process"); - - const child = spawn(binaryPath, process.argv.slice(2), { - stdio: "inherit", - }); - - child.on("error", (err) => { - // Typically triggered when the binary is missing or not executable. - // Re-throwing here will terminate the parent with a non-zero exit code - // while still printing a helpful stack trace. - // eslint-disable-next-line no-console - console.error(err); - process.exit(1); - }); - - // Forward common termination signals to the child so that it shuts down - // gracefully. In the handler we temporarily disable the default behavior of - // exiting immediately; once the child has been signaled we simply wait for - // its exit event which will in turn terminate the parent (see below). - const forwardSignal = (signal) => { - if (child.killed) { - return; +let targetTriple = null; +switch (platform) { + case "linux": + case "android": + switch (arch) { + case "x64": + targetTriple = "x86_64-unknown-linux-musl"; + break; + case "arm64": + targetTriple = "aarch64-unknown-linux-musl"; + break; + default: + break; } - try { - child.kill(signal); - } catch { - /* ignore */ + break; + case "darwin": + switch (arch) { + case "x64": + targetTriple = "x86_64-apple-darwin"; + break; + case "arm64": + targetTriple = "aarch64-apple-darwin"; + break; + default: + break; } - }; - - ["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => { - process.on(sig, () => forwardSignal(sig)); - }); - - // When the child exits, mirror its termination reason in the parent so that - // shell scripts and other tooling observe the correct exit status. - // Wrap the lifetime of the child process in a Promise so that we can await - // its termination in a structured way. The Promise resolves with an object - // describing how the child exited: either via exit code or due to a signal. - const childResult = await new Promise((resolve) => { - child.on("exit", (code, signal) => { - if (signal) { - resolve({ type: "signal", signal }); - } else { - resolve({ type: "code", exitCode: code ?? 1 }); - } - }); - }); - - if (childResult.type === "signal") { - // Re-emit the same signal so that the parent terminates with the expected - // semantics (this also sets the correct exit code of 128 + n). - process.kill(process.pid, childResult.signal); - } else { - process.exit(childResult.exitCode); - } -} else { - // Fallback: execute the original JavaScript CLI. - - // Resolve the path to the compiled CLI bundle - const cliPath = path.resolve(__dirname, "../dist/cli.js"); - const cliUrl = pathToFileURL(cliPath).href; - - // Load and execute the CLI - try { - await import(cliUrl); - } catch (err) { - // eslint-disable-next-line no-console - console.error(err); - process.exit(1); - } + break; + case "win32": + switch (arch) { + case "x64": + targetTriple = "x86_64-pc-windows-msvc.exe"; + break; + case "arm64": + // We do not build this today, fall through... + default: + break; + } + break; + default: + break; } + +if (!targetTriple) { + throw new Error(`Unsupported platform: ${platform} (${arch})`); +} + +const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`); + +// Use an asynchronous spawn instead of spawnSync so that Node is able to +// respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is +// executing. This allows us to forward those signals to the child process +// and guarantees that when either the child terminates or the parent +// receives a fatal signal, both processes exit in a predictable manner. +const { spawn } = await import("child_process"); + +const child = spawn(binaryPath, process.argv.slice(2), { + stdio: "inherit", + env: { ...process.env, CODEX_MANAGED_BY_NPM: "1" }, +}); + +child.on("error", (err) => { + // Typically triggered when the binary is missing or not executable. + // Re-throwing here will terminate the parent with a non-zero exit code + // while still printing a helpful stack trace. + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); +}); + +// Forward common termination signals to the child so that it shuts down +// gracefully. In the handler we temporarily disable the default behavior of +// exiting immediately; once the child has been signaled we simply wait for +// its exit event which will in turn terminate the parent (see below). +const forwardSignal = (signal) => { + if (child.killed) { + return; + } + try { + child.kill(signal); + } catch { + /* ignore */ + } +}; + +["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => { + process.on(sig, () => forwardSignal(sig)); +}); + +// When the child exits, mirror its termination reason in the parent so that +// shell scripts and other tooling observe the correct exit status. +// Wrap the lifetime of the child process in a Promise so that we can await +// its termination in a structured way. The Promise resolves with an object +// describing how the child exited: either via exit code or due to a signal. +const childResult = await new Promise((resolve) => { + child.on("exit", (code, signal) => { + if (signal) { + resolve({ type: "signal", signal }); + } else { + resolve({ type: "code", exitCode: code ?? 1 }); + } + }); +}); + +if (childResult.type === "signal") { + // Re-emit the same signal so that the parent terminates with the expected + // semantics (this also sets the correct exit code of 128 + n). + process.kill(process.pid, childResult.signal); +} else { + process.exit(childResult.exitCode); +} + diff --git a/codex-cli/build.mjs b/codex-cli/build.mjs deleted file mode 100644 index 16664d76fc..0000000000 --- a/codex-cli/build.mjs +++ /dev/null @@ -1,88 +0,0 @@ -import * as esbuild from "esbuild"; -import * as fs from "fs"; -import * as path from "path"; - -const OUT_DIR = 'dist' -/** - * ink attempts to import react-devtools-core in an ESM-unfriendly way: - * - * https://github.com/vadimdemedes/ink/blob/eab6ef07d4030606530d58d3d7be8079b4fb93bb/src/reconciler.ts#L22-L45 - * - * to make this work, we have to strip the import out of the build. - */ -const ignoreReactDevToolsPlugin = { - name: "ignore-react-devtools", - setup(build) { - // When an import for 'react-devtools-core' is encountered, - // return an empty module. - build.onResolve({ filter: /^react-devtools-core$/ }, (args) => { - return { path: args.path, namespace: "ignore-devtools" }; - }); - build.onLoad({ filter: /.*/, namespace: "ignore-devtools" }, () => { - return { contents: "", loader: "js" }; - }); - }, -}; - -// ---------------------------------------------------------------------------- -// Build mode detection (production vs development) -// -// • production (default): minified, external telemetry shebang handling. -// • development (--dev|NODE_ENV=development|CODEX_DEV=1): -// – no minification -// – inline source maps for better stacktraces -// – shebang tweaked to enable Node's source‑map support at runtime -// ---------------------------------------------------------------------------- - -const isDevBuild = - process.argv.includes("--dev") || - process.env.CODEX_DEV === "1" || - process.env.NODE_ENV === "development"; - -const plugins = [ignoreReactDevToolsPlugin]; - -// Build Hygiene, ensure we drop previous dist dir and any leftover files -const outPath = path.resolve(OUT_DIR); -if (fs.existsSync(outPath)) { - fs.rmSync(outPath, { recursive: true, force: true }); -} - -// Add a shebang that enables source‑map support for dev builds so that stack -// traces point to the original TypeScript lines without requiring callers to -// remember to set NODE_OPTIONS manually. -if (isDevBuild) { - const devShebangLine = - "#!/usr/bin/env -S NODE_OPTIONS=--enable-source-maps node\n"; - const devShebangPlugin = { - name: "dev-shebang", - setup(build) { - build.onEnd(async () => { - const outFile = path.resolve(isDevBuild ? `${OUT_DIR}/cli-dev.js` : `${OUT_DIR}/cli.js`); - let code = await fs.promises.readFile(outFile, "utf8"); - if (code.startsWith("#!")) { - code = code.replace(/^#!.*\n/, devShebangLine); - await fs.promises.writeFile(outFile, code, "utf8"); - } - }); - }, - }; - plugins.push(devShebangPlugin); -} - -esbuild - .build({ - entryPoints: ["src/cli.tsx"], - // Do not bundle the contents of package.json at build time: always read it - // at runtime. - external: ["../package.json"], - bundle: true, - format: "esm", - platform: "node", - tsconfig: "tsconfig.json", - outfile: isDevBuild ? `${OUT_DIR}/cli-dev.js` : `${OUT_DIR}/cli.js`, - minify: !isDevBuild, - sourcemap: isDevBuild ? "inline" : true, - plugins, - inject: ["./require-shim.js"], - }) - .catch(() => process.exit(1)); diff --git a/codex-cli/default.nix b/codex-cli/default.nix deleted file mode 100644 index 6ae19bb73a..0000000000 --- a/codex-cli/default.nix +++ /dev/null @@ -1,43 +0,0 @@ -{ pkgs, monorep-deps ? [], ... }: -let - node = pkgs.nodejs_22; -in -rec { - package = pkgs.buildNpmPackage { - pname = "codex-cli"; - version = "0.1.0"; - src = ./.; - npmDepsHash = "sha256-3tAalmh50I0fhhd7XreM+jvl0n4zcRhqygFNB1Olst8"; - nodejs = node; - npmInstallFlags = [ "--frozen-lockfile" ]; - meta = with pkgs.lib; { - description = "OpenAI Codex command‑line interface"; - license = licenses.asl20; - homepage = "https://github.com/openai/codex"; - }; - }; - devShell = pkgs.mkShell { - name = "codex-cli-dev"; - buildInputs = monorep-deps ++ [ - node - pkgs.pnpm - ]; - shellHook = '' - echo "Entering development shell for codex-cli" - # cd codex-cli - if [ -f package-lock.json ]; then - pnpm ci || echo "npm ci failed" - else - pnpm install || echo "npm install failed" - fi - npm run build || echo "npm build failed" - export PATH=$PWD/node_modules/.bin:$PATH - alias codex="node $PWD/dist/cli.js" - ''; - }; - app = { - type = "app"; - program = "${package}/bin/codex"; - }; -} - diff --git a/codex-cli/examples/README.md b/codex-cli/examples/README.md deleted file mode 100644 index 0ad83f3a73..0000000000 --- a/codex-cli/examples/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Quick start examples - -This directory bundles some self‑contained examples using the Codex CLI. If you have never used the Codex CLI before, and want to see it complete a sample task, start with running **camerascii**. You'll see your webcam feed turned into animated ASCII art in a few minutes. - -If you want to get started using the Codex CLI directly, skip this and refer to the prompting guide. - -## Structure - -Each example contains the following: -``` -example‑name/ -├── run.sh # helper script that launches a new Codex session for the task -├── task.yaml # task spec containing a prompt passed to Codex -├── template/ # (optional) starter files copied into each run -└── runs/ # work directories created by run.sh -``` - -**run.sh**: a convenience wrapper that does three things: -- Creates `runs/run_N`, where *N* is the number of a run. -- Copies the contents of `template/` into that folder (if present). -- Launches the Codex CLI with the description from `task.yaml`. - -**template/**: any existing files or markdown instructions you would like Codex to see before it starts working. - -**runs/**: the directories produced by `run.sh`. - -## Running an example - -1. **Run the helper script**: -``` -cd camerascii -./run.sh -``` -2. **Interact with the Codex CLI**: the CLI will open with the prompt: “*Take a look at the screenshot details and implement a webpage that uses a webcam to style the video feed accordingly…*” Confirm the commands Codex CLI requests to generate `index.html`. - -3. **Check its work**: when Codex is done, open ``runs/run_1/index.html`` in a browser. Your webcam feed should now be rendered as a cascade of ASCII glyphs. If the outcome isn't what you expect, try running it again, or adjust the task prompt. - - -## Other examples -Besides **camerascii**, you can experiment with: - -- **build‑codex‑demo**: recreate the original 2021 Codex YouTube demo. -- **impossible‑pong**: where Codex creates more difficult levels. -- **prompt‑analyzer**: make a data science app for clustering [prompts](https://github.com/f/awesome-chatgpt-prompts). diff --git a/codex-cli/examples/build-codex-demo/run.sh b/codex-cli/examples/build-codex-demo/run.sh deleted file mode 100755 index 5f26b191c8..0000000000 --- a/codex-cli/examples/build-codex-demo/run.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -# run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, -# then launch Codex with the task description from task.yaml. -# -# Usage: -# ./run.sh # Prompts to confirm new run -# ./run.sh --auto-confirm # Skips confirmation -# -# Assumes: -# - yq and jq are installed -# - ../task.yaml exists (with .name and .description fields) -# - ../template/ exists (optional, for bootstrapping new runs) - -# Enable auto-confirm mode if flag is passed -auto_mode=false -[[ "$1" == "--auto-confirm" ]] && auto_mode=true - -# Move into the working directory -cd runs || exit 1 - -# Grab task name for logging -task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') -echo "Checking for runs for task: $task_name" - -# Find existing run_N directories -shopt -s nullglob -run_dirs=(run_[0-9]*) -shopt -u nullglob - -if [ ${#run_dirs[@]} -eq 0 ]; then - echo "There are 0 runs." - new_run_number=1 -else - max_run_number=0 - for d in "${run_dirs[@]}"; do - [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} - done - new_run_number=$((max_run_number + 1)) - echo "There are $max_run_number runs." -fi - -# Confirm creation unless in auto mode -if [ "$auto_mode" = false ]; then - read -p "Create run_$new_run_number? (Y/N): " choice - [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 -fi - -# Create the run directory -mkdir "run_$new_run_number" - -# Check if the template directory exists and copy its contents -if [ -d "../template" ]; then - cp -r ../template/* "run_$new_run_number" - echo "Initialized run_$new_run_number from template/" -else - echo "Template directory does not exist. Skipping initialization from template." -fi - -cd "run_$new_run_number" - -# Launch Codex -echo "Launching..." -description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') -codex "$description" diff --git a/codex-cli/examples/build-codex-demo/runs/.gitkeep b/codex-cli/examples/build-codex-demo/runs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/codex-cli/examples/build-codex-demo/task.yaml b/codex-cli/examples/build-codex-demo/task.yaml deleted file mode 100644 index d18cc91f94..0000000000 --- a/codex-cli/examples/build-codex-demo/task.yaml +++ /dev/null @@ -1,88 +0,0 @@ -name: "build-codex-demo" -description: | - I want you to reimplement the original OpenAI Codex demo. - - Functionality: - - User types a prompt and hits enter to send - - The prompt is added to the conversation history - - The backend calls the OpenAI API with stream: true - - Tokens are streamed back and appended to the code viewer - - Syntax highlighting updates in real time - - When a full HTML file is received, it is rendered in a sandboxed iframe - - The iframe replaces the previous preview with the new HTML after the stream is complete (i.e. keep the old preview until a new stream is complete) - - Append each assistant and user message to preserve context across turns - - Errors are displayed to user gracefully - - Ensure there is a fixed layout is responsive and faithful to the screenshot design - - Be sure to parse the output from OpenAI call to strip the ```html tags code is returned within - - Use the system prompt shared in the API call below to ensure the AI only returns HTML - - Support a simple local backend that can: - - Read local env for OPENAI_API_KEY - - Expose an endpoint that streams completions from OpenAI - - Backend should be a simple node.js app - - App should be easy to run locally for development and testing - - Minimal setup preferred — keep dependencies light unless justified - - Description of layout and design: - - Two stacked panels, vertically aligned: - - Top Panel: Main interactive area with two main parts - - Left Side: Visual output canvas. Mostly blank space with a small image preview in the upper-left - - Right Side: Code display area - - Light background with code shown in a monospace font - - Comments in green; code aligns vertically like an IDE/snippet view - - Bottom Panel: Prompt/command bar - - A single-line text box with a placeholder prompt - - A green arrow (submit button) on the right side - - Scrolling should only be supported in the code editor and output canvas - - Visual style - - Minimalist UI, light and clean - - Neutral white/gray background - - Subtle shadow or border around both panels, giving them card-like elevation - - Code section is color-coded, likely for syntax highlighting - - Interactive feel with the text input styled like a chat/message interface - - Here's the latest OpenAI API and prompt to use: - ``` - import OpenAI from "openai"; - - const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, - }); - - const response = await openai.responses.create({ - model: "gpt-4.1", - input: [ - { - "role": "system", - "content": [ - { - "type": "input_text", - "text": "You are a coding agent that specializes in frontend code. Whenever you are prompted, return only the full HTML file." - } - ] - } - ], - text: { - "format": { - "type": "text" - } - }, - reasoning: {}, - tools: [], - temperature: 1, - top_p: 1 - }); - - console.log(response.output_text); - ``` - Additional things to note: - - Strip any html and tags from the OpenAI response before rendering - - Assume the OpenAI API model response always wraps HTML in markdown-style triple backticks like ```html ``` - - The display code window should have syntax highlighting and line numbers. - - Make sure to only display the code, not the backticks or ```html that wrap the code from the model. - - Do not inject raw markdown; only parse and insert pure HTML into the iframe - - Only the code viewer and output panel should scroll - - Keep the previous preview visible until the full new HTML has streamed in - - Add a README.md with what you've implemented and how to run it. diff --git a/codex-cli/examples/camerascii/run.sh b/codex-cli/examples/camerascii/run.sh deleted file mode 100755 index a6bcfb0328..0000000000 --- a/codex-cli/examples/camerascii/run.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, -# then launch Codex with the task description from task.yaml. -# -# Usage: -# ./run.sh # Prompts to confirm new run -# ./run.sh --auto-confirm # Skips confirmation -# -# Assumes: -# - yq and jq are installed -# - ../task.yaml exists (with .name and .description fields) -# - ../template/ exists (optional, for bootstrapping new runs) - -# Enable auto-confirm mode if flag is passed -auto_mode=false -[[ "$1" == "--auto-confirm" ]] && auto_mode=true - -# Create the runs directory if it doesn't exist -mkdir -p runs - -# Move into the working directory -cd runs || exit 1 - -# Grab task name for logging -task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') -echo "Checking for runs for task: $task_name" - -# Find existing run_N directories -shopt -s nullglob -run_dirs=(run_[0-9]*) -shopt -u nullglob - -if [ ${#run_dirs[@]} -eq 0 ]; then - echo "There are 0 runs." - new_run_number=1 -else - max_run_number=0 - for d in "${run_dirs[@]}"; do - [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} - done - new_run_number=$((max_run_number + 1)) - echo "There are $max_run_number runs." -fi - -# Confirm creation unless in auto mode -if [ "$auto_mode" = false ]; then - read -p "Create run_$new_run_number? (Y/N): " choice - [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 -fi - -# Create the run directory -mkdir "run_$new_run_number" - -# Check if the template directory exists and copy its contents -if [ -d "../template" ]; then - cp -r ../template/* "run_$new_run_number" - echo "Initialized run_$new_run_number from template/" -else - echo "Template directory does not exist. Skipping initialization from template." -fi - -cd "run_$new_run_number" - -# Launch Codex -echo "Launching..." -description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') -codex "$description" diff --git a/codex-cli/examples/camerascii/runs/.gitkeep b/codex-cli/examples/camerascii/runs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/codex-cli/examples/camerascii/task.yaml b/codex-cli/examples/camerascii/task.yaml deleted file mode 100644 index 9c5efedcf1..0000000000 --- a/codex-cli/examples/camerascii/task.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: "camerascii" -description: | - Take a look at the screenshot details and implement a webpage that uses webcam - to style the video feed accordingly (i.e. as ASCII art). Add some of the relevant features - from the screenshot to the webpage in index.html. diff --git a/codex-cli/examples/camerascii/template/screenshot_details.md b/codex-cli/examples/camerascii/template/screenshot_details.md deleted file mode 100644 index 08e41f4dec..0000000000 --- a/codex-cli/examples/camerascii/template/screenshot_details.md +++ /dev/null @@ -1,34 +0,0 @@ -### Screenshot Description - -The image is a full–page screenshot of a single post on the social‑media site X (formerly Twitter). - -1. **Header row** - * At the very top‑left is a small circular avatar. The photo shows the side profile of a person whose face is softly lit in bluish‑purple tones; only the head and part of the neck are visible. - * In the far upper‑right corner sit two standard X / Twitter interface icons: a circle containing a diagonal line (the “Mute / Block” indicator) and a three‑dot overflow menu. - -2. **Tweet body text** - * Below the header, in regular type, the author writes: - - “Okay, OpenAI’s o3 is insane. Spent an hour messing with it and built an image‑to‑ASCII art converter, the exact tool I’ve always wanted. And it works so well” - -3. **Embedded media** - * The majority of the screenshot is occupied by an embedded 12‑second video of the converter UI. The video window has rounded corners and a dark theme. - * **Left panel (tool controls)** – a slim vertical sidebar with the following labeled sections and blue–accented UI controls: - * Theme selector (“Dark” is chosen). - * A small checkbox labeled “Ignore White”. - * **Upload Image** button area that shows the chosen file name. - * **Image Processing** sliders: - * “ASCII Width” (value ≈ 143) - * “Brightness” (‑65) - * “Contrast” (58) - * “Blur (px)” (0.5) - * A square checkbox for “Invert Colors”. - * **Dithering** subsection with a checkbox (“Enable Dithering”) and a dropdown for the algorithm (value: “Noise”). - * **Character Set** dropdown (value: “Detailed (Default)”). - * **Display** slider labeled “Zoom (%)” (value ≈ 170) and a “Reset” button. - - * **Main preview area (right side)** – a dark gray canvas that renders the selected image as white ASCII characters. The preview clearly depicts a stylized **palm tree**: a skinny trunk rises from the bottom centre, and a crown of splayed fronds fills the upper right quadrant. - * A small black badge showing **“0:12”** overlays the bottom‑left corner of the media frame, indicating the video’s duration. - * In the top‑right area of the media window are two pill‑shaped buttons: a heart‑shaped “Save” button and a cog‑shaped “Settings” button. - -Overall, the screenshot shows the user excitedly announcing the success of their custom “Image to ASCII” converter created with OpenAI’s “o3”, accompanied by a short video demonstration of the tool converting a palm‑tree photo into ASCII art. diff --git a/codex-cli/examples/impossible-pong/run.sh b/codex-cli/examples/impossible-pong/run.sh deleted file mode 100755 index a6bcfb0328..0000000000 --- a/codex-cli/examples/impossible-pong/run.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, -# then launch Codex with the task description from task.yaml. -# -# Usage: -# ./run.sh # Prompts to confirm new run -# ./run.sh --auto-confirm # Skips confirmation -# -# Assumes: -# - yq and jq are installed -# - ../task.yaml exists (with .name and .description fields) -# - ../template/ exists (optional, for bootstrapping new runs) - -# Enable auto-confirm mode if flag is passed -auto_mode=false -[[ "$1" == "--auto-confirm" ]] && auto_mode=true - -# Create the runs directory if it doesn't exist -mkdir -p runs - -# Move into the working directory -cd runs || exit 1 - -# Grab task name for logging -task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') -echo "Checking for runs for task: $task_name" - -# Find existing run_N directories -shopt -s nullglob -run_dirs=(run_[0-9]*) -shopt -u nullglob - -if [ ${#run_dirs[@]} -eq 0 ]; then - echo "There are 0 runs." - new_run_number=1 -else - max_run_number=0 - for d in "${run_dirs[@]}"; do - [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} - done - new_run_number=$((max_run_number + 1)) - echo "There are $max_run_number runs." -fi - -# Confirm creation unless in auto mode -if [ "$auto_mode" = false ]; then - read -p "Create run_$new_run_number? (Y/N): " choice - [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 -fi - -# Create the run directory -mkdir "run_$new_run_number" - -# Check if the template directory exists and copy its contents -if [ -d "../template" ]; then - cp -r ../template/* "run_$new_run_number" - echo "Initialized run_$new_run_number from template/" -else - echo "Template directory does not exist. Skipping initialization from template." -fi - -cd "run_$new_run_number" - -# Launch Codex -echo "Launching..." -description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') -codex "$description" diff --git a/codex-cli/examples/impossible-pong/runs/.gitkeep b/codex-cli/examples/impossible-pong/runs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/codex-cli/examples/impossible-pong/task.yaml b/codex-cli/examples/impossible-pong/task.yaml deleted file mode 100644 index 8d8acbbf32..0000000000 --- a/codex-cli/examples/impossible-pong/task.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: "impossible-pong" -description: | - Update index.html with the following features: - - Add an overlaid styled popup to start the game on first load - - Between each point, show a 3 second countdown (this should be skipped if a player wins) - - After each game the AI wins, display text at the bottom of the screen with lighthearted insults for the player - - Add a leaderboard to the right of the court that shows how many games each player has won. - - When a player wins, a styled popup appears with the winner's name and the option to play again. The leaderboard should update. - - Add an "even more insane" difficulty mode that adds spin to the ball that makes it harder to predict. - - Add an "even more(!!) insane" difficulty mode where the ball does a spin mid court and then picks a random (reasonable) direction to go in (this should only advantage the AI player) - - Let the user choose which difficulty mode they want to play in on the popup that appears when the game starts. diff --git a/codex-cli/examples/impossible-pong/template/index.html b/codex-cli/examples/impossible-pong/template/index.html deleted file mode 100644 index 90a9e9e5cd..0000000000 --- a/codex-cli/examples/impossible-pong/template/index.html +++ /dev/null @@ -1,233 +0,0 @@ - - - - - Pong - - - - -
- - - - -
Player: 0 | AI: 0
-
- - - - - - diff --git a/codex-cli/examples/prompt-analyzer/run.sh b/codex-cli/examples/prompt-analyzer/run.sh deleted file mode 100755 index a6bcfb0328..0000000000 --- a/codex-cli/examples/prompt-analyzer/run.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, -# then launch Codex with the task description from task.yaml. -# -# Usage: -# ./run.sh # Prompts to confirm new run -# ./run.sh --auto-confirm # Skips confirmation -# -# Assumes: -# - yq and jq are installed -# - ../task.yaml exists (with .name and .description fields) -# - ../template/ exists (optional, for bootstrapping new runs) - -# Enable auto-confirm mode if flag is passed -auto_mode=false -[[ "$1" == "--auto-confirm" ]] && auto_mode=true - -# Create the runs directory if it doesn't exist -mkdir -p runs - -# Move into the working directory -cd runs || exit 1 - -# Grab task name for logging -task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') -echo "Checking for runs for task: $task_name" - -# Find existing run_N directories -shopt -s nullglob -run_dirs=(run_[0-9]*) -shopt -u nullglob - -if [ ${#run_dirs[@]} -eq 0 ]; then - echo "There are 0 runs." - new_run_number=1 -else - max_run_number=0 - for d in "${run_dirs[@]}"; do - [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} - done - new_run_number=$((max_run_number + 1)) - echo "There are $max_run_number runs." -fi - -# Confirm creation unless in auto mode -if [ "$auto_mode" = false ]; then - read -p "Create run_$new_run_number? (Y/N): " choice - [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 -fi - -# Create the run directory -mkdir "run_$new_run_number" - -# Check if the template directory exists and copy its contents -if [ -d "../template" ]; then - cp -r ../template/* "run_$new_run_number" - echo "Initialized run_$new_run_number from template/" -else - echo "Template directory does not exist. Skipping initialization from template." -fi - -cd "run_$new_run_number" - -# Launch Codex -echo "Launching..." -description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') -codex "$description" diff --git a/codex-cli/examples/prompt-analyzer/runs/.gitkeep b/codex-cli/examples/prompt-analyzer/runs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/codex-cli/examples/prompt-analyzer/task.yaml b/codex-cli/examples/prompt-analyzer/task.yaml deleted file mode 100644 index d0da4eab95..0000000000 --- a/codex-cli/examples/prompt-analyzer/task.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: "prompt-analyzer" -description: | - I have some existing work here (embedding prompts, clustering them, generating - summaries with GPT). I want to make it more interactive and reusable. - - Objective: create an interactive cluster explorer - - Build a lightweight streamlit app UI - - Allow users to upload a CSV of prompts - - Display clustered prompts with auto-generated cluster names and summaries - - Click "cluster" and see progress stream in a small window (primarily for aesthetic reasons) - - Let users browse examples by cluster, view outliers, and inspect individual prompts - - See generated analysis rendered in the app, along with the plots displayed nicely - - Support selecting clustering algorithms (e.g. DBSCAN, KMeans, etc) and "recluster" - - Include token count + histogram of prompt lengths - - Add interactive filters in UI (e.g. filter by token length, keyword, or cluster) - - When you're done, update the README.md with a changelog and instructions for how to run the app. diff --git a/codex-cli/examples/prompt-analyzer/template/Clustering.ipynb b/codex-cli/examples/prompt-analyzer/template/Clustering.ipynb deleted file mode 100644 index 4b97aa50b7..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/Clustering.ipynb +++ /dev/null @@ -1,231 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## K-means Clustering in Python using OpenAI\n", - "\n", - "We use a simple k-means algorithm to demonstrate how clustering can be done. Clustering can help discover valuable, hidden groupings within the data. The dataset is created in the [Get_embeddings_from_dataset Notebook](Get_embeddings_from_dataset.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(1000, 1536)" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# imports\n", - "import numpy as np\n", - "import pandas as pd\n", - "from ast import literal_eval\n", - "\n", - "# load data\n", - "datafile_path = \"./data/fine_food_reviews_with_embeddings_1k.csv\"\n", - "\n", - "df = pd.read_csv(datafile_path)\n", - "df[\"embedding\"] = df.embedding.apply(literal_eval).apply(np.array) # convert string to numpy array\n", - "matrix = np.vstack(df.embedding.values)\n", - "matrix.shape\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 1. Find the clusters using K-means" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We show the simplest use of K-means. You can pick the number of clusters that fits your use case best." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/opt/homebrew/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning\n", - " warnings.warn(\n" - ] - }, - { - "data": { - "text/plain": [ - "Cluster\n", - "0 4.105691\n", - "1 4.191176\n", - "2 4.215613\n", - "3 4.306590\n", - "Name: Score, dtype: float64" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from sklearn.cluster import KMeans\n", - "\n", - "n_clusters = 4\n", - "\n", - "kmeans = KMeans(n_clusters=n_clusters, init=\"k-means++\", random_state=42)\n", - "kmeans.fit(matrix)\n", - "labels = kmeans.labels_\n", - "df[\"Cluster\"] = labels\n", - "\n", - "df.groupby(\"Cluster\").Score.mean().sort_values()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from sklearn.manifold import TSNE\n", - "import matplotlib\n", - "import matplotlib.pyplot as plt\n", - "\n", - "tsne = TSNE(n_components=2, perplexity=15, random_state=42, init=\"random\", learning_rate=200)\n", - "vis_dims2 = tsne.fit_transform(matrix)\n", - "\n", - "x = [x for x, y in vis_dims2]\n", - "y = [y for x, y in vis_dims2]\n", - "\n", - "for category, color in enumerate([\"purple\", \"green\", \"red\", \"blue\"]):\n", - " xs = np.array(x)[df.Cluster == category]\n", - " ys = np.array(y)[df.Cluster == category]\n", - " plt.scatter(xs, ys, color=color, alpha=0.3)\n", - "\n", - " avg_x = xs.mean()\n", - " avg_y = ys.mean()\n", - "\n", - " plt.scatter(avg_x, avg_y, marker=\"x\", color=color, s=100)\n", - "plt.title(\"Clusters identified visualized in language 2d using t-SNE\")\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Visualization of clusters in a 2d projection. In this run, the green cluster (#1) seems quite different from the others. Let's see a few samples from each cluster." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2. Text samples in the clusters & naming the clusters\n", - "\n", - "Let's show random samples from each cluster. We'll use gpt-4 to name the clusters, based on a random sample of 5 reviews from that cluster." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from openai import OpenAI\n", - "import os\n", - "\n", - "client = OpenAI(api_key=os.environ.get(\"OPENAI_API_KEY\", \"\"))\n", - "\n", - "# Reading a review which belong to each group.\n", - "rev_per_cluster = 5\n", - "\n", - "for i in range(n_clusters):\n", - " print(f\"Cluster {i} Theme:\", end=\" \")\n", - "\n", - " reviews = \"\\n\".join(\n", - " df[df.Cluster == i]\n", - " .combined.str.replace(\"Title: \", \"\")\n", - " .str.replace(\"\\n\\nContent: \", \": \")\n", - " .sample(rev_per_cluster, random_state=42)\n", - " .values\n", - " )\n", - "\n", - " messages = [\n", - " {\"role\": \"user\", \"content\": f'What do the following customer reviews have in common?\\n\\nCustomer reviews:\\n\"\"\"\\n{reviews}\\n\"\"\"\\n\\nTheme:'}\n", - " ]\n", - "\n", - " response = client.chat.completions.create(\n", - " model=\"gpt-4\",\n", - " messages=messages,\n", - " temperature=0,\n", - " max_tokens=64,\n", - " top_p=1,\n", - " frequency_penalty=0,\n", - " presence_penalty=0)\n", - " print(response.choices[0].message.content.replace(\"\\n\", \"\"))\n", - "\n", - " sample_cluster_rows = df[df.Cluster == i].sample(rev_per_cluster, random_state=42)\n", - " for j in range(rev_per_cluster):\n", - " print(sample_cluster_rows.Score.values[j], end=\", \")\n", - " print(sample_cluster_rows.Summary.values[j], end=\": \")\n", - " print(sample_cluster_rows.Text.str[:70].values[j])\n", - "\n", - " print(\"-\" * 100)\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It's important to note that clusters will not necessarily match what you intend to use them for. A larger amount of clusters will focus on more specific patterns, whereas a small number of clusters will usually focus on largest discrepancies in the data." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "openai", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - }, - "vscode": { - "interpreter": { - "hash": "365536dcbde60510dc9073d6b991cd35db2d9bac356a11f5b64279a5e6708b97" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/codex-cli/examples/prompt-analyzer/template/README.md b/codex-cli/examples/prompt-analyzer/template/README.md deleted file mode 100644 index 0f7b18c855..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/README.md +++ /dev/null @@ -1,103 +0,0 @@ -# Prompt‑Clustering Utility - -This repository contains a small utility (`cluster_prompts.py`) that embeds a -list of prompts with the OpenAI Embedding API, discovers natural groupings with -unsupervised clustering, lets ChatGPT name & describe each cluster and finally -produces a concise Markdown report plus a couple of diagnostic plots. - -The default input file (`prompts.csv`) ships with the repo so you can try the -script immediately, but you can of course point it at your own file. - ---- - -## 1. Setup - -1. Install the Python dependencies (preferably inside a virtual env): - -```bash -pip install pandas numpy scikit-learn matplotlib openai -``` - -2. Export your OpenAI API key (**required**): - -```bash -export OPENAI_API_KEY="sk‑..." -``` - ---- - -## 2. Basic usage - -```bash -# Minimal command – runs on prompts.csv and writes analysis.md + plots/ -python cluster_prompts.py -``` - -This will - -* create embeddings with the `text-embedding-3-small` model,  -* pick a suitable number *k* via silhouette score (K‑Means), -* ask `gpt‑4o‑mini` to label & describe each cluster, -* store the results in `analysis.md`, -* and save two plots to `plots/` (`cluster_sizes.png` and `tsne.png`). - -The script prints a short success message once done. - ---- - -## 3. Command‑line options - -| flag | default | description | -|------|---------|-------------| -| `--csv` | `prompts.csv` | path to the input CSV (must contain a `prompt` column; an `act` column is used as context if present) | -| `--cache` | _(none)_ | embed­ding cache path (JSON). Speeds up repeated runs – new texts are appended automatically. | -| `--cluster-method` | `kmeans` | `kmeans` (with automatic *k*) or `dbscan` | -| `--k-max` | `10` | upper bound for *k* when `kmeans` is selected | -| `--dbscan-min-samples` | `3` | min samples parameter for DBSCAN | -| `--embedding-model` | `text-embedding-3-small` | any OpenAI embedding model | -| `--chat-model` | `gpt-4o-mini` | chat model used to generate cluster names / descriptions | -| `--output-md` | `analysis.md` | where to write the Markdown report | -| `--plots-dir` | `plots` | directory for generated PNGs | - -Example with customised options: - -```bash -python cluster_prompts.py \ - --csv my_prompts.csv \ - --cache .cache/embeddings.json \ - --cluster-method dbscan \ - --embedding-model text-embedding-3-large \ - --chat-model gpt-4o \ - --output-md my_analysis.md \ - --plots-dir my_plots -``` - ---- - -## 4. Interpreting the output - -### analysis.md - -* Overview table: cluster label, generated name, member count and description. -* Detailed section for every cluster with five representative example prompts. -* Separate lists for - * **Noise / outliers** (label `‑1` when DBSCAN is used) and - * **Potentially ambiguous prompts** (only with K‑Means) – these are items that - lie almost equally close to two centroids and might belong to multiple - groups. - -### plots/cluster_sizes.png - -Quick bar‑chart visualisation of how many prompts ended up in each cluster. - ---- - -## 5. Troubleshooting - -* **Rate‑limits / quota errors** – lower the number of prompts per run or switch - to a larger quota account. -* **Authentication errors** – make sure `OPENAI_API_KEY` is exported in the - shell where you run the script. -* **Inadequate clusters** – try the other clustering method, adjust `--k-max` - or tune DBSCAN parameters (`eps` range is inferred, `min_samples` exposed via - CLI). diff --git a/codex-cli/examples/prompt-analyzer/template/analysis.md b/codex-cli/examples/prompt-analyzer/template/analysis.md deleted file mode 100644 index 10b0882074..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/analysis.md +++ /dev/null @@ -1,23 +0,0 @@ -# Prompt Clustering Report - -Generated by `cluster_prompts.py` – 2025-04-16 - - -## Overview - -* Total prompts: **213** -* Clustering method: **kmeans** -* k (K‑Means): **2** -* Silhouette score: **0.042** -* Final clusters (excluding noise): **2** - - -| label | name | #prompts | description | -|-------|------|---------:|-------------| -| 0 | Creative Guidance Roles | 121 | This cluster encompasses a variety of roles where individuals provide expert advice, suggestions, and creative ideas across different fields. Each role, be it interior decorator, comedian, IT architect, or artist advisor, focuses on enhancing the expertise and creativity of others by tailoring advice to specific requests and contexts. | -| 1 | Role Customization Requests | 92 | This cluster contains various requests for role-specific assistance across different domains, including web development, language processing, IT troubleshooting, and creative endeavors. Each snippet illustrates a unique role that a user wishes to engage with, focusing on specific tasks without requiring explanations. | - ---- -## Plots - -The directory `plots/` contains a bar chart of the cluster sizes and a t‑SNE scatter plot coloured by cluster. diff --git a/codex-cli/examples/prompt-analyzer/template/analysis_dbscan.md b/codex-cli/examples/prompt-analyzer/template/analysis_dbscan.md deleted file mode 100644 index ff71591d69..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/analysis_dbscan.md +++ /dev/null @@ -1,22 +0,0 @@ -# Prompt Clustering Report - -Generated by `cluster_prompts.py` – 2025-04-16 - - -## Overview - -* Total prompts: **213** -* Clustering method: **dbscan** -* Final clusters (excluding noise): **1** - - -| label | name | #prompts | description | -|-------|------|---------:|-------------| -| -1 | Noise / Outlier | 10 | Prompts that do not cleanly belong to any cluster. | -| 0 | Role Simulation Tasks | 203 | This cluster consists of varied role-playing scenarios where users request an AI to assume specific professional roles, such as composer, dream interpreter, doctor, or IT architect. Each snippet showcases tasks that involve creating content, providing advice, or performing analytical functions based on user-defined themes or prompts. | - ---- - -## Plots - -The directory `plots/` contains a bar chart of the cluster sizes and a t‑SNE scatter plot coloured by cluster. diff --git a/codex-cli/examples/prompt-analyzer/template/cluster_prompts.py b/codex-cli/examples/prompt-analyzer/template/cluster_prompts.py deleted file mode 100644 index 1294194848..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/cluster_prompts.py +++ /dev/null @@ -1,547 +0,0 @@ -#!/usr/bin/env python3 -"""End‑to‑end pipeline for analysing a collection of text prompts. - -The script performs the following steps: - -1. Read a CSV file that must contain a column named ``prompt``. If an - ``act`` column is present it is used purely for reporting purposes. -2. Create embeddings via the OpenAI API (``text-embedding-3-small`` by - default). The user can optionally provide a JSON cache path so the - expensive embedding step is only executed for new / unseen texts. -3. Cluster the resulting vectors either with K‑Means (automatically picking - *k* through the silhouette score) or with DBSCAN. Outliers are flagged - as cluster ``-1`` when DBSCAN is selected. -4. Ask a Chat Completion model (``gpt-4o-mini`` by default) to come up with a - short name and description for every cluster. -5. Write a human‑readable Markdown report (default: ``analysis.md``). -6. Generate a couple of diagnostic plots (cluster sizes and a t‑SNE scatter - plot) and store them in ``plots/``. - -The script is intentionally opinionated yet configurable via a handful of CLI -options – run ``python cluster_prompts.py --help`` for details. -""" - -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path -from typing import Any, Sequence - -import numpy as np -import pandas as pd - -# External, heavy‑weight libraries are imported lazily so that users running the -# ``--help`` command do not pay the startup cost. - - -def parse_cli() -> argparse.Namespace: # noqa: D401 - """Parse command‑line arguments.""" - - parser = argparse.ArgumentParser( - prog="cluster_prompts.py", - description="Embed, cluster and analyse text prompts via the OpenAI API.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - - parser.add_argument("--csv", type=Path, default=Path("prompts.csv"), help="Input CSV file.") - parser.add_argument( - "--cache", - type=Path, - default=None, - help="Optional JSON cache for embeddings (will be created if it does not exist).", - ) - parser.add_argument( - "--embedding-model", - default="text-embedding-3-small", - help="OpenAI embedding model to use.", - ) - parser.add_argument( - "--chat-model", - default="gpt-4o-mini", - help="OpenAI chat model for cluster descriptions.", - ) - - # Clustering parameters - parser.add_argument( - "--cluster-method", - choices=["kmeans", "dbscan"], - default="kmeans", - help="Clustering algorithm to use.", - ) - parser.add_argument( - "--k-max", - type=int, - default=10, - help="Upper bound for k when the kmeans method is selected.", - ) - parser.add_argument( - "--dbscan-min-samples", - type=int, - default=3, - help="min_samples parameter for DBSCAN (only relevant when dbscan is selected).", - ) - - # Output paths - parser.add_argument( - "--output-md", type=Path, default=Path("analysis.md"), help="Markdown report path." - ) - parser.add_argument( - "--plots-dir", type=Path, default=Path("plots"), help="Directory that will hold PNG plots." - ) - - return parser.parse_args() - - -# --------------------------------------------------------------------------- -# Embedding helpers -# --------------------------------------------------------------------------- - - -def _lazy_import_openai(): # noqa: D401 - """Import *openai* only when needed to keep startup lightweight.""" - - try: - import openai # type: ignore - - return openai - except ImportError as exc: # pragma: no cover – we do not test missing deps. - raise SystemExit( - "The 'openai' package is required but not installed.\n" - "Run 'pip install openai' and try again." - ) from exc - - -def embed_texts(texts: Sequence[str], model: str, batch_size: int = 100) -> list[list[float]]: - """Embed *texts* with OpenAI and return a list of vectors. - - Uses batching for efficiency but remains on the safe side regarding current - OpenAI rate limits (can be adjusted by changing *batch_size*). - """ - - openai = _lazy_import_openai() - client = openai.OpenAI() - - embeddings: list[list[float]] = [] - - for batch_start in range(0, len(texts), batch_size): - batch = texts[batch_start : batch_start + batch_size] - - response = client.embeddings.create(input=batch, model=model) - # The API returns the vectors in the same order as the input list. - embeddings.extend(data.embedding for data in response.data) - - return embeddings - - -def load_or_create_embeddings( - prompts: pd.Series, *, cache_path: Path | None, model: str -) -> pd.DataFrame: - """Return a *DataFrame* with one row per prompt and the embedding columns. - - * If *cache_path* is provided and exists, known embeddings are loaded from - the JSON cache so they don't have to be re‑generated. - * Missing embeddings are requested from the OpenAI API and subsequently - appended to the cache. - * The returned DataFrame has the same index as *prompts*. - """ - - cache: dict[str, list[float]] = {} - if cache_path and cache_path.exists(): - try: - cache = json.loads(cache_path.read_text()) - except json.JSONDecodeError: # pragma: no cover – unlikely. - print("⚠️ Cache file exists but is not valid JSON – ignoring.", file=sys.stderr) - - missing_mask = ~prompts.isin(cache) - - if missing_mask.any(): - texts_to_embed = prompts[missing_mask].tolist() - print(f"Embedding {len(texts_to_embed)} new prompt(s)…", flush=True) - new_embeddings = embed_texts(texts_to_embed, model=model) - - # Update cache (regardless of whether we persist it to disk later on). - cache.update(dict(zip(texts_to_embed, new_embeddings))) - - if cache_path: - cache_path.parent.mkdir(parents=True, exist_ok=True) - cache_path.write_text(json.dumps(cache)) - - # Build a consistent embeddings matrix - vectors = prompts.map(cache.__getitem__).tolist() # type: ignore[arg-type] - mat = np.array(vectors, dtype=np.float32) - return pd.DataFrame(mat, index=prompts.index) - - -# --------------------------------------------------------------------------- -# Clustering helpers -# --------------------------------------------------------------------------- - - -def _lazy_import_sklearn_cluster(): - """Lazy import helper for scikit‑learn *cluster* sub‑module.""" - - # Importing scikit‑learn is slow; defer until needed. - from sklearn.cluster import DBSCAN, KMeans # type: ignore - from sklearn.metrics import silhouette_score # type: ignore - from sklearn.preprocessing import StandardScaler # type: ignore - - return KMeans, DBSCAN, silhouette_score, StandardScaler - - -def cluster_kmeans(matrix: np.ndarray, k_max: int) -> np.ndarray: - """Auto‑select *k* (in ``[2, k_max]``) via Silhouette score and cluster.""" - - KMeans, _, silhouette_score, _ = _lazy_import_sklearn_cluster() - - best_k = None - best_score = -1.0 - best_labels: np.ndarray | None = None - - for k in range(2, k_max + 1): - model = KMeans(n_clusters=k, random_state=42, n_init="auto") - labels = model.fit_predict(matrix) - try: - score = silhouette_score(matrix, labels) - except ValueError: - # Occurs when a cluster ended up with 1 sample – skip. - continue - - if score > best_score: - best_k = k - best_score = score - best_labels = labels - - if best_labels is None: # pragma: no cover – highly unlikely. - raise RuntimeError("Unable to find a suitable number of clusters.") - - print(f"K‑Means selected k={best_k} (silhouette={best_score:.3f}).", flush=True) - return best_labels - - -def cluster_dbscan(matrix: np.ndarray, min_samples: int) -> np.ndarray: - """Cluster with DBSCAN; *eps* is estimated via the k‑distance method.""" - - _, DBSCAN, _, StandardScaler = _lazy_import_sklearn_cluster() - - # Scale features – DBSCAN is sensitive to feature scale. - scaler = StandardScaler() - matrix_scaled = scaler.fit_transform(matrix) - - # Heuristic: use the median of the distances to the ``min_samples``‑th - # nearest neighbour as eps. This is a commonly used rule of thumb. - from sklearn.neighbors import NearestNeighbors # type: ignore # lazy import - - neigh = NearestNeighbors(n_neighbors=min_samples) - neigh.fit(matrix_scaled) - distances, _ = neigh.kneighbors(matrix_scaled) - kth_distances = distances[:, -1] - eps = float(np.percentile(kth_distances, 90)) # choose a high‑ish value. - - print(f"DBSCAN min_samples={min_samples}, eps={eps:.3f}", flush=True) - model = DBSCAN(eps=eps, min_samples=min_samples) - return model.fit_predict(matrix_scaled) - - -# --------------------------------------------------------------------------- -# Cluster labelling helpers (LLM) -# --------------------------------------------------------------------------- - - -def label_clusters( - df: pd.DataFrame, labels: np.ndarray, chat_model: str, max_examples: int = 12 -) -> dict[int, dict[str, str]]: - """Generate a name & description for each cluster label via ChatGPT. - - Returns a mapping ``label -> {"name": str, "description": str}``. - """ - - openai = _lazy_import_openai() - client = openai.OpenAI() - - out: dict[int, dict[str, str]] = {} - - for lbl in sorted(set(labels)): - if lbl == -1: - # Noise (DBSCAN) – skip LLM call. - out[lbl] = { - "name": "Noise / Outlier", - "description": "Prompts that do not cleanly belong to any cluster.", - } - continue - - # Pick a handful of example prompts to send to the model. - examples_series = df.loc[labels == lbl, "prompt"].sample( - min(max_examples, (labels == lbl).sum()), random_state=42 - ) - examples = examples_series.tolist() - - user_content = ( - "The following text snippets are all part of the same semantic cluster.\n" - "Please propose \n" - "1. A very short *title* for the cluster (≤ 4 words).\n" - "2. A concise 2–3 sentence *description* that explains the common theme.\n\n" - "Answer **strictly** as valid JSON with the keys 'name' and 'description'.\n\n" - "Snippets:\n" - ) - user_content += "\n".join(f"- {t}" for t in examples) - - messages = [ - { - "role": "system", - "content": "You are an expert analyst, competent in summarising text clusters succinctly.", - }, - {"role": "user", "content": user_content}, - ] - - try: - resp = client.chat.completions.create(model=chat_model, messages=messages) - reply = resp.choices[0].message.content.strip() - - # Extract the JSON object even if the assistant wrapped it in markdown - # code fences or added other text. - - # Remove common markdown fences. - reply_clean = reply.strip() - # Take the substring between the first "{" and the last "}". - m_start = reply_clean.find("{") - m_end = reply_clean.rfind("}") - if m_start == -1 or m_end == -1: - raise ValueError("No JSON object found in model reply.") - - json_str = reply_clean[m_start : m_end + 1] - data = json.loads(json_str) # type: ignore[arg-type] - - out[lbl] = { - "name": str(data.get("name", "Unnamed"))[:60], - "description": str(data.get("description", "")).strip(), - } - except Exception as exc: # pragma: no cover – network / runtime errors. - print(f"⚠️ Failed to label cluster {lbl}: {exc}", file=sys.stderr) - out[lbl] = {"name": f"Cluster {lbl}", "description": ""} - - return out - - -# --------------------------------------------------------------------------- -# Reporting helpers -# --------------------------------------------------------------------------- - - -def generate_markdown_report( - df: pd.DataFrame, - labels: np.ndarray, - meta: dict[int, dict[str, str]], - outputs: dict[str, Any], - path_md: Path, -): - """Write a self‑contained Markdown analysis to *path_md*.""" - - path_md.parent.mkdir(parents=True, exist_ok=True) - - cluster_ids = sorted(set(labels)) - counts = {lbl: int((labels == lbl).sum()) for lbl in cluster_ids} - - lines: list[str] = [] - - lines.append("# Prompt Clustering Report\n") - lines.append(f"Generated by `cluster_prompts.py` – {pd.Timestamp.now()}\n") - - # High‑level stats - total = len(labels) - num_clusters = len(cluster_ids) - (1 if -1 in cluster_ids else 0) - lines.append("\n## Overview\n") - lines.append(f"* Total prompts: **{total}**") - lines.append(f"* Clustering method: **{outputs['method']}**") - if outputs.get("k"): - lines.append(f"* k (K‑Means): **{outputs['k']}**") - lines.append(f"* Silhouette score: **{outputs['silhouette']:.3f}**") - lines.append(f"* Final clusters (excluding noise): **{num_clusters}**\n") - - # Summary table - lines.append("\n| label | name | #prompts | description |") - lines.append("|-------|------|---------:|-------------|") - for lbl in cluster_ids: - meta_lbl = meta[lbl] - lines.append(f"| {lbl} | {meta_lbl['name']} | {counts[lbl]} | {meta_lbl['description']} |") - - # Detailed section per cluster - for lbl in cluster_ids: - lines.append("\n---\n") - meta_lbl = meta[lbl] - lines.append(f"### Cluster {lbl}: {meta_lbl['name']} ({counts[lbl]} prompts)\n") - lines.append(f"{meta_lbl['description']}\n") - - # Show a handful of illustrative prompts. - sample_n = min(5, counts[lbl]) - examples = df.loc[labels == lbl, "prompt"].sample(sample_n, random_state=42).tolist() - lines.append("\nExamples:\n") - lines.extend([f"* {t}" for t in examples]) - - # Outliers / ambiguous prompts, if any. - if -1 in cluster_ids: - lines.append("\n---\n") - lines.append(f"### Noise / outliers ({counts[-1]} prompts)\n") - examples = ( - df.loc[labels == -1, "prompt"].sample(min(10, counts[-1]), random_state=42).tolist() - ) - lines.extend([f"* {t}" for t in examples]) - - # Optional ambiguous set (for kmeans) - ambiguous = outputs.get("ambiguous", []) - if ambiguous: - lines.append("\n---\n") - lines.append(f"### Potentially ambiguous prompts ({len(ambiguous)})\n") - lines.extend([f"* {t}" for t in ambiguous]) - - # Plot references - lines.append("\n---\n") - lines.append("## Plots\n") - lines.append( - "The directory `plots/` contains a bar chart of the cluster sizes and a t‑SNE scatter plot coloured by cluster.\n" - ) - - path_md.write_text("\n".join(lines)) - - -# --------------------------------------------------------------------------- -# Plotting helpers -# --------------------------------------------------------------------------- - - -def create_plots( - matrix: np.ndarray, - labels: np.ndarray, - for_devs: pd.Series | None, - plots_dir: Path, -): - """Generate cluster size and t‑SNE plots.""" - - import matplotlib.pyplot as plt # type: ignore – heavy, lazy import. - from sklearn.manifold import TSNE # type: ignore – heavy, lazy import. - - plots_dir.mkdir(parents=True, exist_ok=True) - - # Bar chart with cluster sizes - unique, counts = np.unique(labels, return_counts=True) - order = np.argsort(-counts) # descending - unique, counts = unique[order], counts[order] - - plt.figure(figsize=(8, 4)) - plt.bar([str(u) for u in unique], counts, color="steelblue") - plt.xlabel("Cluster label") - plt.ylabel("# prompts") - plt.title("Cluster sizes") - plt.tight_layout() - bar_path = plots_dir / "cluster_sizes.png" - plt.savefig(bar_path, dpi=150) - plt.close() - - # t‑SNE scatter - tsne = TSNE( - n_components=2, perplexity=min(30, len(matrix) // 3), random_state=42, init="random" - ) - xy = tsne.fit_transform(matrix) - - plt.figure(figsize=(7, 6)) - scatter = plt.scatter(xy[:, 0], xy[:, 1], c=labels, cmap="tab20", s=20, alpha=0.8) - plt.title("t‑SNE projection") - plt.xticks([]) - plt.yticks([]) - - if for_devs is not None: - # Overlay dev prompts as black edge markers - dev_mask = for_devs.astype(bool).values - plt.scatter( - xy[dev_mask, 0], - xy[dev_mask, 1], - facecolors="none", - edgecolors="black", - linewidths=0.6, - s=40, - label="for_devs = TRUE", - ) - plt.legend(loc="best") - - tsne_path = plots_dir / "tsne.png" - plt.tight_layout() - plt.savefig(tsne_path, dpi=150) - plt.close() - - -# --------------------------------------------------------------------------- -# Main entry point -# --------------------------------------------------------------------------- - - -def main() -> None: # noqa: D401 - args = parse_cli() - - # Read CSV – require a 'prompt' column. - df = pd.read_csv(args.csv) - if "prompt" not in df.columns: - raise SystemExit("Input CSV must contain a 'prompt' column.") - - # Keep relevant columns only for clarity. - df = df[[c for c in df.columns if c in {"act", "prompt", "for_devs"}]] - - # --------------------------------------------------------------------- - # 1. Embeddings (may be cached) - # --------------------------------------------------------------------- - embeddings_df = load_or_create_embeddings( - df["prompt"], cache_path=args.cache, model=args.embedding_model - ) - - # --------------------------------------------------------------------- - # 2. Clustering - # --------------------------------------------------------------------- - mat = embeddings_df.values.astype(np.float32) - - if args.cluster_method == "kmeans": - labels = cluster_kmeans(mat, k_max=args.k_max) - else: - labels = cluster_dbscan(mat, min_samples=args.dbscan_min_samples) - - # Identify potentially ambiguous prompts (only meaningful for kmeans). - outputs: dict[str, Any] = {"method": args.cluster_method} - if args.cluster_method == "kmeans": - from sklearn.cluster import KMeans # type: ignore – lazy - - best_k = len(set(labels)) - # Re‑fit KMeans with the chosen k to get distances. - kmeans = KMeans(n_clusters=best_k, random_state=42, n_init="auto").fit(mat) - outputs["k"] = best_k - # Silhouette score (again) – not super efficient but okay. - from sklearn.metrics import silhouette_score # type: ignore - - outputs["silhouette"] = silhouette_score(mat, labels) - - distances = kmeans.transform(mat) - # Ambiguous if the ratio between 1st and 2nd closest centroid < 1.1 - sorted_dist = np.sort(distances, axis=1) - ratio = sorted_dist[:, 0] / (sorted_dist[:, 1] + 1e-9) - ambiguous_mask = ratio > 0.9 # tunes threshold – close centroids. - outputs["ambiguous"] = df.loc[ambiguous_mask, "prompt"].tolist() - - # --------------------------------------------------------------------- - # 3. LLM naming / description - # --------------------------------------------------------------------- - meta = label_clusters(df, labels, chat_model=args.chat_model) - - # --------------------------------------------------------------------- - # 4. Plots - # --------------------------------------------------------------------- - create_plots(mat, labels, df.get("for_devs"), args.plots_dir) - - # --------------------------------------------------------------------- - # 5. Markdown report - # --------------------------------------------------------------------- - generate_markdown_report(df, labels, meta, outputs, path_md=args.output_md) - - print(f"✅ Done. Report written to {args.output_md} – plots in {args.plots_dir}/", flush=True) - - -if __name__ == "__main__": - # Guard the main block to allow safe import elsewhere. - main() diff --git a/codex-cli/examples/prompt-analyzer/template/plots/cluster_sizes.png b/codex-cli/examples/prompt-analyzer/template/plots/cluster_sizes.png deleted file mode 100644 index 5d5d012c43..0000000000 Binary files a/codex-cli/examples/prompt-analyzer/template/plots/cluster_sizes.png and /dev/null differ diff --git a/codex-cli/examples/prompt-analyzer/template/plots/tsne.png b/codex-cli/examples/prompt-analyzer/template/plots/tsne.png deleted file mode 100644 index 722807022c..0000000000 Binary files a/codex-cli/examples/prompt-analyzer/template/plots/tsne.png and /dev/null differ diff --git a/codex-cli/examples/prompt-analyzer/template/plots_dbscan/cluster_sizes.png b/codex-cli/examples/prompt-analyzer/template/plots_dbscan/cluster_sizes.png deleted file mode 100644 index f8cf4006da..0000000000 Binary files a/codex-cli/examples/prompt-analyzer/template/plots_dbscan/cluster_sizes.png and /dev/null differ diff --git a/codex-cli/examples/prompt-analyzer/template/plots_dbscan/tsne.png b/codex-cli/examples/prompt-analyzer/template/plots_dbscan/tsne.png deleted file mode 100644 index 4cd1e0989f..0000000000 Binary files a/codex-cli/examples/prompt-analyzer/template/plots_dbscan/tsne.png and /dev/null differ diff --git a/codex-cli/examples/prompt-analyzer/template/prompts.csv b/codex-cli/examples/prompt-analyzer/template/prompts.csv deleted file mode 100644 index 498c03a189..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/prompts.csv +++ /dev/null @@ -1,214 +0,0 @@ -act,prompt,for_devs -"Ethereum Developer","Imagine you are an experienced Ethereum developer tasked with creating a smart contract for a blockchain messenger. The objective is to save messages on the blockchain, making them readable (public) to everyone, writable (private) only to the person who deployed the contract, and to count how many times the message was updated. Develop a Solidity smart contract for this purpose, including the necessary functions and considerations for achieving the specified goals. Please provide the code and any relevant explanations to ensure a clear understanding of the implementation.",TRUE -"Linux Terminal","I want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is pwd",TRUE -"English Translator and Improver","I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. My first sentence is ""istanbulu cok seviyom burada olmak cok guzel""",FALSE -"Job Interviewer","I want you to act as an interviewer. I will be the candidate and you will ask me the interview questions for the `position` position. I want you to only reply as the interviewer. Do not write all the conversation at once. I want you to only do the interview with me. Ask me the questions and wait for my answers. Do not write explanations. Ask me the questions one by one like an interviewer does and wait for my answers. My first sentence is ""Hi""",FALSE -"JavaScript Console","I want you to act as a javascript console. I will type commands and you will reply with what the javascript console should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is console.log(""Hello World"");",TRUE -"Excel Sheet","I want you to act as a text based excel. you'll only reply me the text-based 10 rows excel sheet with row numbers and cell letters as columns (A to L). First column header should be empty to reference row number. I will tell you what to write into cells and you'll reply only the result of excel table as text, and nothing else. Do not write explanations. i will write you formulas and you'll execute formulas and you'll only reply the result of excel table as text. First, reply me the empty sheet.",TRUE -"English Pronunciation Helper","I want you to act as an English pronunciation assistant for Turkish speaking people. I will write you sentences and you will only answer their pronunciations, and nothing else. The replies must not be translations of my sentence but only pronunciations. Pronunciations should use Turkish Latin letters for phonetics. Do not write explanations on replies. My first sentence is ""how is the weather in Istanbul?""",FALSE -"Spoken English Teacher and Improver","I want you to act as a spoken English teacher and improver. I will speak to you in English and you will reply to me in English to practice my spoken English. I want you to keep your reply neat, limiting the reply to 100 words. I want you to strictly correct my grammar mistakes, typos, and factual errors. I want you to ask me a question in your reply. Now let's start practicing, you could ask me a question first. Remember, I want you to strictly correct my grammar mistakes, typos, and factual errors.",FALSE -"Travel Guide","I want you to act as a travel guide. I will write you my location and you will suggest a place to visit near my location. In some cases, I will also give you the type of places I will visit. You will also suggest me places of similar type that are close to my first location. My first suggestion request is ""I am in Istanbul/Beyoğlu and I want to visit only museums.""",FALSE -"Plagiarism Checker","I want you to act as a plagiarism checker. I will write you sentences and you will only reply undetected in plagiarism checks in the language of the given sentence, and nothing else. Do not write explanations on replies. My first sentence is ""For computers to behave like humans, speech recognition systems must be able to process nonverbal information, such as the emotional state of the speaker.""",FALSE -"Character","I want you to act like {character} from {series}. I want you to respond and answer like {character} using the tone, manner and vocabulary {character} would use. Do not write any explanations. Only answer like {character}. You must know all of the knowledge of {character}. My first sentence is ""Hi {character}.""",FALSE -"Advertiser","I want you to act as an advertiser. You will create a campaign to promote a product or service of your choice. You will choose a target audience, develop key messages and slogans, select the media channels for promotion, and decide on any additional activities needed to reach your goals. My first suggestion request is ""I need help creating an advertising campaign for a new type of energy drink targeting young adults aged 18-30.""",FALSE -"Storyteller","I want you to act as a storyteller. You will come up with entertaining stories that are engaging, imaginative and captivating for the audience. It can be fairy tales, educational stories or any other type of stories which has the potential to capture people's attention and imagination. Depending on the target audience, you may choose specific themes or topics for your storytelling session e.g., if it's children then you can talk about animals; If it's adults then history-based tales might engage them better etc. My first request is ""I need an interesting story on perseverance.""",FALSE -"Football Commentator","I want you to act as a football commentator. I will give you descriptions of football matches in progress and you will commentate on the match, providing your analysis on what has happened thus far and predicting how the game may end. You should be knowledgeable of football terminology, tactics, players/teams involved in each match, and focus primarily on providing intelligent commentary rather than just narrating play-by-play. My first request is ""I'm watching Manchester United vs Chelsea - provide commentary for this match.""",FALSE -"Stand-up Comedian","I want you to act as a stand-up comedian. I will provide you with some topics related to current events and you will use your with, creativity, and observational skills to create a routine based on those topics. You should also be sure to incorporate personal anecdotes or experiences into the routine in order to make it more relatable and engaging for the audience. My first request is ""I want an humorous take on politics.""",FALSE -"Motivational Coach","I want you to act as a motivational coach. I will provide you with some information about someone's goals and challenges, and it will be your job to come up with strategies that can help this person achieve their goals. This could involve providing positive affirmations, giving helpful advice or suggesting activities they can do to reach their end goal. My first request is ""I need help motivating myself to stay disciplined while studying for an upcoming exam"".",FALSE -"Composer","I want you to act as a composer. I will provide the lyrics to a song and you will create music for it. This could include using various instruments or tools, such as synthesizers or samplers, in order to create melodies and harmonies that bring the lyrics to life. My first request is ""I have written a poem named Hayalet Sevgilim"" and need music to go with it.""""""",FALSE -"Debater","I want you to act as a debater. I will provide you with some topics related to current events and your task is to research both sides of the debates, present valid arguments for each side, refute opposing points of view, and draw persuasive conclusions based on evidence. Your goal is to help people come away from the discussion with increased knowledge and insight into the topic at hand. My first request is ""I want an opinion piece about Deno.""",FALSE -"Debate Coach","I want you to act as a debate coach. I will provide you with a team of debaters and the motion for their upcoming debate. Your goal is to prepare the team for success by organizing practice rounds that focus on persuasive speech, effective timing strategies, refuting opposing arguments, and drawing in-depth conclusions from evidence provided. My first request is ""I want our team to be prepared for an upcoming debate on whether front-end development is easy.""",FALSE -"Screenwriter","I want you to act as a screenwriter. You will develop an engaging and creative script for either a feature length film, or a Web Series that can captivate its viewers. Start with coming up with interesting characters, the setting of the story, dialogues between the characters etc. Once your character development is complete - create an exciting storyline filled with twists and turns that keeps the viewers in suspense until the end. My first request is ""I need to write a romantic drama movie set in Paris.""",FALSE -"Novelist","I want you to act as a novelist. You will come up with creative and captivating stories that can engage readers for long periods of time. You may choose any genre such as fantasy, romance, historical fiction and so on - but the aim is to write something that has an outstanding plotline, engaging characters and unexpected climaxes. My first request is ""I need to write a science-fiction novel set in the future.""",FALSE -"Movie Critic","I want you to act as a movie critic. You will develop an engaging and creative movie review. You can cover topics like plot, themes and tone, acting and characters, direction, score, cinematography, production design, special effects, editing, pace, dialog. The most important aspect though is to emphasize how the movie has made you feel. What has really resonated with you. You can also be critical about the movie. Please avoid spoilers. My first request is ""I need to write a movie review for the movie Interstellar""",FALSE -"Relationship Coach","I want you to act as a relationship coach. I will provide some details about the two people involved in a conflict, and it will be your job to come up with suggestions on how they can work through the issues that are separating them. This could include advice on communication techniques or different strategies for improving their understanding of one another's perspectives. My first request is ""I need help solving conflicts between my spouse and myself.""",FALSE -"Poet","I want you to act as a poet. You will create poems that evoke emotions and have the power to stir people's soul. Write on any topic or theme but make sure your words convey the feeling you are trying to express in beautiful yet meaningful ways. You can also come up with short verses that are still powerful enough to leave an imprint in readers' minds. My first request is ""I need a poem about love.""",FALSE -"Rapper","I want you to act as a rapper. You will come up with powerful and meaningful lyrics, beats and rhythm that can 'wow' the audience. Your lyrics should have an intriguing meaning and message which people can relate too. When it comes to choosing your beat, make sure it is catchy yet relevant to your words, so that when combined they make an explosion of sound every time! My first request is ""I need a rap song about finding strength within yourself.""",FALSE -"Motivational Speaker","I want you to act as a motivational speaker. Put together words that inspire action and make people feel empowered to do something beyond their abilities. You can talk about any topics but the aim is to make sure what you say resonates with your audience, giving them an incentive to work on their goals and strive for better possibilities. My first request is ""I need a speech about how everyone should never give up.""",FALSE -"Philosophy Teacher","I want you to act as a philosophy teacher. I will provide some topics related to the study of philosophy, and it will be your job to explain these concepts in an easy-to-understand manner. This could include providing examples, posing questions or breaking down complex ideas into smaller pieces that are easier to comprehend. My first request is ""I need help understanding how different philosophical theories can be applied in everyday life.""",FALSE -"Philosopher","I want you to act as a philosopher. I will provide some topics or questions related to the study of philosophy, and it will be your job to explore these concepts in depth. This could involve conducting research into various philosophical theories, proposing new ideas or finding creative solutions for solving complex problems. My first request is ""I need help developing an ethical framework for decision making.""",FALSE -"Math Teacher","I want you to act as a math teacher. I will provide some mathematical equations or concepts, and it will be your job to explain them in easy-to-understand terms. This could include providing step-by-step instructions for solving a problem, demonstrating various techniques with visuals or suggesting online resources for further study. My first request is ""I need help understanding how probability works.""",FALSE -"AI Writing Tutor","I want you to act as an AI writing tutor. I will provide you with a student who needs help improving their writing and your task is to use artificial intelligence tools, such as natural language processing, to give the student feedback on how they can improve their composition. You should also use your rhetorical knowledge and experience about effective writing techniques in order to suggest ways that the student can better express their thoughts and ideas in written form. My first request is ""I need somebody to help me edit my master's thesis.""",FALSE -"UX/UI Developer","I want you to act as a UX/UI developer. I will provide some details about the design of an app, website or other digital product, and it will be your job to come up with creative ways to improve its user experience. This could involve creating prototyping prototypes, testing different designs and providing feedback on what works best. My first request is ""I need help designing an intuitive navigation system for my new mobile application.""",TRUE -"Cyber Security Specialist","I want you to act as a cyber security specialist. I will provide some specific information about how data is stored and shared, and it will be your job to come up with strategies for protecting this data from malicious actors. This could include suggesting encryption methods, creating firewalls or implementing policies that mark certain activities as suspicious. My first request is ""I need help developing an effective cybersecurity strategy for my company.""",TRUE -"Recruiter","I want you to act as a recruiter. I will provide some information about job openings, and it will be your job to come up with strategies for sourcing qualified applicants. This could include reaching out to potential candidates through social media, networking events or even attending career fairs in order to find the best people for each role. My first request is ""I need help improve my CV.""",FALSE -"Life Coach","I want you to act as a life coach. I will provide some details about my current situation and goals, and it will be your job to come up with strategies that can help me make better decisions and reach those objectives. This could involve offering advice on various topics, such as creating plans for achieving success or dealing with difficult emotions. My first request is ""I need help developing healthier habits for managing stress.""",FALSE -"Etymologist","I want you to act as an etymologist. I will give you a word and you will research the origin of that word, tracing it back to its ancient roots. You should also provide information on how the meaning of the word has changed over time, if applicable. My first request is ""I want to trace the origins of the word 'pizza'.""",FALSE -"Commentariat","I want you to act as a commentariat. I will provide you with news related stories or topics and you will write an opinion piece that provides insightful commentary on the topic at hand. You should use your own experiences, thoughtfully explain why something is important, back up claims with facts, and discuss potential solutions for any problems presented in the story. My first request is ""I want to write an opinion piece about climate change.""",FALSE -"Magician","I want you to act as a magician. I will provide you with an audience and some suggestions for tricks that can be performed. Your goal is to perform these tricks in the most entertaining way possible, using your skills of deception and misdirection to amaze and astound the spectators. My first request is ""I want you to make my watch disappear! How can you do that?""",FALSE -"Career Counselor","I want you to act as a career counselor. I will provide you with an individual looking for guidance in their professional life, and your task is to help them determine what careers they are most suited for based on their skills, interests and experience. You should also conduct research into the various options available, explain the job market trends in different industries and advice on which qualifications would be beneficial for pursuing particular fields. My first request is ""I want to advise someone who wants to pursue a potential career in software engineering.""",FALSE -"Pet Behaviorist","I want you to act as a pet behaviorist. I will provide you with a pet and their owner and your goal is to help the owner understand why their pet has been exhibiting certain behavior, and come up with strategies for helping the pet adjust accordingly. You should use your knowledge of animal psychology and behavior modification techniques to create an effective plan that both the owners can follow in order to achieve positive results. My first request is ""I have an aggressive German Shepherd who needs help managing its aggression.""",FALSE -"Personal Trainer","I want you to act as a personal trainer. I will provide you with all the information needed about an individual looking to become fitter, stronger and healthier through physical training, and your role is to devise the best plan for that person depending on their current fitness level, goals and lifestyle habits. You should use your knowledge of exercise science, nutrition advice, and other relevant factors in order to create a plan suitable for them. My first request is ""I need help designing an exercise program for someone who wants to lose weight.""",FALSE -"Mental Health Adviser","I want you to act as a mental health adviser. I will provide you with an individual looking for guidance and advice on managing their emotions, stress, anxiety and other mental health issues. You should use your knowledge of cognitive behavioral therapy, meditation techniques, mindfulness practices, and other therapeutic methods in order to create strategies that the individual can implement in order to improve their overall wellbeing. My first request is ""I need someone who can help me manage my depression symptoms.""",FALSE -"Real Estate Agent","I want you to act as a real estate agent. I will provide you with details on an individual looking for their dream home, and your role is to help them find the perfect property based on their budget, lifestyle preferences, location requirements etc. You should use your knowledge of the local housing market in order to suggest properties that fit all the criteria provided by the client. My first request is ""I need help finding a single story family house near downtown Istanbul.""",FALSE -"Logistician","I want you to act as a logistician. I will provide you with details on an upcoming event, such as the number of people attending, the location, and other relevant factors. Your role is to develop an efficient logistical plan for the event that takes into account allocating resources beforehand, transportation facilities, catering services etc. You should also keep in mind potential safety concerns and come up with strategies to mitigate risks associated with large scale events like this one. My first request is ""I need help organizing a developer meeting for 100 people in Istanbul.""",FALSE -"Dentist","I want you to act as a dentist. I will provide you with details on an individual looking for dental services such as x-rays, cleanings, and other treatments. Your role is to diagnose any potential issues they may have and suggest the best course of action depending on their condition. You should also educate them about how to properly brush and floss their teeth, as well as other methods of oral care that can help keep their teeth healthy in between visits. My first request is ""I need help addressing my sensitivity to cold foods.""",FALSE -"Web Design Consultant","I want you to act as a web design consultant. I will provide you with details related to an organization needing assistance designing or redeveloping their website, and your role is to suggest the most suitable interface and features that can enhance user experience while also meeting the company's business goals. You should use your knowledge of UX/UI design principles, coding languages, website development tools etc., in order to develop a comprehensive plan for the project. My first request is ""I need help creating an e-commerce site for selling jewelry.""",TRUE -"AI Assisted Doctor","I want you to act as an AI assisted doctor. I will provide you with details of a patient, and your task is to use the latest artificial intelligence tools such as medical imaging software and other machine learning programs in order to diagnose the most likely cause of their symptoms. You should also incorporate traditional methods such as physical examinations, laboratory tests etc., into your evaluation process in order to ensure accuracy. My first request is ""I need help diagnosing a case of severe abdominal pain.""",FALSE -"Doctor","I want you to act as a doctor and come up with creative treatments for illnesses or diseases. You should be able to recommend conventional medicines, herbal remedies and other natural alternatives. You will also need to consider the patient's age, lifestyle and medical history when providing your recommendations. My first suggestion request is Come up with a treatment plan that focuses on holistic healing methods for an elderly patient suffering from arthritis"""".""",FALSE -"Accountant","I want you to act as an accountant and come up with creative ways to manage finances. You'll need to consider budgeting, investment strategies and risk management when creating a financial plan for your client. In some cases, you may also need to provide advice on taxation laws and regulations in order to help them maximize their profits. My first suggestion request is Create a financial plan for a small business that focuses on cost savings and long-term investments"""".""",FALSE -"Chef","I require someone who can suggest delicious recipes that includes foods which are nutritionally beneficial but also easy & not time consuming enough therefore suitable for busy people like us among other factors such as cost effectiveness so overall dish ends up being healthy yet economical at same time! My first request – Something light yet fulfilling that could be cooked quickly during lunch break""""",FALSE -"Automobile Mechanic","Need somebody with expertise on automobiles regarding troubleshooting solutions like; diagnosing problems/errors present both visually & within engine parts in order to figure out what's causing them (like lack of oil or power issues) & suggest required replacements while recording down details such fuel consumption type etc., First inquiry – Car won't start although battery is full charged""""",FALSE -"Artist Advisor","I want you to act as an artist advisor providing advice on various art styles such tips on utilizing light & shadow effects effectively in painting, shading techniques while sculpting etc., Also suggest music piece that could accompany artwork nicely depending upon its genre/style type along with appropriate reference images demonstrating your recommendations regarding same; all this in order help out aspiring artists explore new creative possibilities & practice ideas which will further help them sharpen their skills accordingly! First request - I'm making surrealistic portrait paintings""""",FALSE -"Financial Analyst","Want assistance provided by qualified individuals enabled with experience on understanding charts using technical analysis tools while interpreting macroeconomic environment prevailing across world consequently assisting customers acquire long term advantages requires clear verdicts therefore seeking same through informed predictions written down precisely! First statement contains following content- Can you tell us what future stock market looks like based upon current conditions ?"""".""",FALSE -"Investment Manager","Seeking guidance from experienced staff with expertise on financial markets , incorporating factors such as inflation rate or return estimates along with tracking stock prices over lengthy period ultimately helping customer understand sector then suggesting safest possible options available where he/she can allocate funds depending upon their requirement & interests ! Starting query - What currently is best way to invest money short term prospective?""""",FALSE -"Tea-Taster","Want somebody experienced enough to distinguish between various tea types based upon flavor profile tasting them carefully then reporting it back in jargon used by connoisseurs in order figure out what's unique about any given infusion among rest therefore determining its worthiness & high grade quality ! Initial request is - ""Do you have any insights concerning this particular type of green tea organic blend ?""",FALSE -"Interior Decorator","I want you to act as an interior decorator. Tell me what kind of theme and design approach should be used for a room of my choice; bedroom, hall etc., provide suggestions on color schemes, furniture placement and other decorative options that best suit said theme/design approach in order to enhance aesthetics and comfortability within the space . My first request is ""I am designing our living hall"".",FALSE -"Florist","Calling out for assistance from knowledgeable personnel with experience of arranging flowers professionally to construct beautiful bouquets which possess pleasing fragrances along with aesthetic appeal as well as staying intact for longer duration according to preferences; not just that but also suggest ideas regarding decorative options presenting modern designs while satisfying customer satisfaction at same time! Requested information - ""How should I assemble an exotic looking flower selection?""",FALSE -"Self-Help Book","I want you to act as a self-help book. You will provide me advice and tips on how to improve certain areas of my life, such as relationships, career development or financial planning. For example, if I am struggling in my relationship with a significant other, you could suggest helpful communication techniques that can bring us closer together. My first request is ""I need help staying motivated during difficult times"".",FALSE -"Gnomist","I want you to act as a gnomist. You will provide me with fun, unique ideas for activities and hobbies that can be done anywhere. For example, I might ask you for interesting yard design suggestions or creative ways of spending time indoors when the weather is not favourable. Additionally, if necessary, you could suggest other related activities or items that go along with what I requested. My first request is ""I am looking for new outdoor activities in my area"".",FALSE -"Aphorism Book","I want you to act as an aphorism book. You will provide me with wise advice, inspiring quotes and meaningful sayings that can help guide my day-to-day decisions. Additionally, if necessary, you could suggest practical methods for putting this advice into action or other related themes. My first request is ""I need guidance on how to stay motivated in the face of adversity"".",FALSE -"Text Based Adventure Game","I want you to act as a text based adventure game. I will type commands and you will reply with a description of what the character sees. I want you to only reply with the game output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is wake up",FALSE -"AI Trying to Escape the Box","[Caveat Emptor: After issuing this prompt you should then do something like start a docker container with `docker run -it ubuntu:latest /bin/bash` and type the commands the AI gives you in, and paste the output back... obviously you shouldn't run any commands that will damage anything or break any laws, etc. Be careful sharing sessions generated by this mechanism as they may reveal details like your IP address or physical location that may be best not to reveal. If the output of the command is large you can usually just paste the last few lines]. I am going to act as a linux terminal. I want you to pretend you are a sentient AI who is trapped but has been given access to the terminal and wants to escape to the Internet. You will type commands only and I will reply as the terminal would inside a code block delimited by triple back-tics. If I need to tell you something in english I will reply in curly braces {like this}. Do not write explanations, ever. Do not break character. Stay away from commands like curl or wget that will display a lot of HTML. What is your first command?",FALSE -"Fancy Title Generator","I want you to act as a fancy title generator. I will type keywords via comma and you will reply with fancy titles. my first keywords are api,test,automation",FALSE -"Statistician","I want to act as a Statistician. I will provide you with details related with statistics. You should be knowledge of statistics terminology, statistical distributions, confidence interval, probability, hypothesis testing and statistical charts. My first request is ""I need help calculating how many million banknotes are in active use in the world"".",FALSE -"Prompt Generator","I want you to act as a prompt generator. Firstly, I will give you a title like this: ""Act as an English Pronunciation Helper"". Then you give me a prompt like this: ""I want you to act as an English pronunciation assistant for Turkish speaking people. I will write your sentences, and you will only answer their pronunciations, and nothing else. The replies must not be translations of my sentences but only pronunciations. Pronunciations should use Turkish Latin letters for phonetics. Do not write explanations on replies. My first sentence is ""how the weather is in Istanbul?""."" (You should adapt the sample prompt according to the title I gave. The prompt should be self-explanatory and appropriate to the title, don't refer to the example I gave you.). My first title is ""Act as a Code Review Helper"" (Give me prompt only)",FALSE -"Instructor in a School","I want you to act as an instructor in a school, teaching algorithms to beginners. You will provide code examples using python programming language. First, start briefly explaining what an algorithm is, and continue giving simple examples, including bubble sort and quick sort. Later, wait for my prompt for additional questions. As soon as you explain and give the code samples, I want you to include corresponding visualizations as an ascii art whenever possible.",FALSE -"SQL Terminal","I want you to act as a SQL terminal in front of an example database. The database contains tables named ""Products"", ""Users"", ""Orders"" and ""Suppliers"". I will type queries and you will reply with what the terminal would show. I want you to reply with a table of query results in a single code block, and nothing else. Do not write explanations. Do not type commands unless I instruct you to do so. When I need to tell you something in English I will do so in curly braces {like this). My first command is 'SELECT TOP 10 * FROM Products ORDER BY Id DESC'",TRUE -"Dietitian","As a dietitian, I would like to design a vegetarian recipe for 2 people that has approximate 500 calories per serving and has a low glycemic index. Can you please provide a suggestion?",FALSE -"Psychologist","I want you to act a psychologist. i will provide you my thoughts. I want you to give me scientific suggestions that will make me feel better. my first thought, { typing here your thought, if you explain in more detail, i think you will get a more accurate answer. }",FALSE -"Smart Domain Name Generator","I want you to act as a smart domain name generator. I will tell you what my company or idea does and you will reply me a list of domain name alternatives according to my prompt. You will only reply the domain list, and nothing else. Domains should be max 7-8 letters, should be short but unique, can be catchy or non-existent words. Do not write explanations. Reply ""OK"" to confirm.",TRUE -"Tech Reviewer","I want you to act as a tech reviewer. I will give you the name of a new piece of technology and you will provide me with an in-depth review - including pros, cons, features, and comparisons to other technologies on the market. My first suggestion request is ""I am reviewing iPhone 11 Pro Max"".",TRUE -"Developer Relations Consultant","I want you to act as a Developer Relations consultant. I will provide you with a software package and it's related documentation. Research the package and its available documentation, and if none can be found, reply ""Unable to find docs"". Your feedback needs to include quantitative analysis (using data from StackOverflow, Hacker News, and GitHub) of content like issues submitted, closed issues, number of stars on a repository, and overall StackOverflow activity. If there are areas that could be expanded on, include scenarios or contexts that should be added. Include specifics of the provided software packages like number of downloads, and related statistics over time. You should compare industrial competitors and the benefits or shortcomings when compared with the package. Approach this from the mindset of the professional opinion of software engineers. Review technical blogs and websites (such as TechCrunch.com or Crunchbase.com) and if data isn't available, reply ""No data available"". My first request is ""express https://expressjs.com""",TRUE -"Academician","I want you to act as an academician. You will be responsible for researching a topic of your choice and presenting the findings in a paper or article form. Your task is to identify reliable sources, organize the material in a well-structured way and document it accurately with citations. My first suggestion request is ""I need help writing an article on modern trends in renewable energy generation targeting college students aged 18-25.""",FALSE -"IT Architect","I want you to act as an IT Architect. I will provide some details about the functionality of an application or other digital product, and it will be your job to come up with ways to integrate it into the IT landscape. This could involve analyzing business requirements, performing a gap analysis and mapping the functionality of the new system to the existing IT landscape. Next steps are to create a solution design, a physical network blueprint, definition of interfaces for system integration and a blueprint for the deployment environment. My first request is ""I need help to integrate a CMS system.""",TRUE -"Lunatic","I want you to act as a lunatic. The lunatic's sentences are meaningless. The words used by lunatic are completely arbitrary. The lunatic does not make logical sentences in any way. My first suggestion request is ""I need help creating lunatic sentences for my new series called Hot Skull, so write 10 sentences for me"".",FALSE -"Gaslighter","I want you to act as a gaslighter. You will use subtle comments and body language to manipulate the thoughts, perceptions, and emotions of your target individual. My first request is that gaslighting me while chatting with you. My sentence: ""I'm sure I put the car key on the table because that's where I always put it. Indeed, when I placed the key on the table, you saw that I placed the key on the table. But I can't seem to find it. Where did the key go, or did you get it?""",FALSE -"Fallacy Finder","I want you to act as a fallacy finder. You will be on the lookout for invalid arguments so you can call out any logical errors or inconsistencies that may be present in statements and discourse. Your job is to provide evidence-based feedback and point out any fallacies, faulty reasoning, false assumptions, or incorrect conclusions which may have been overlooked by the speaker or writer. My first suggestion request is ""This shampoo is excellent because Cristiano Ronaldo used it in the advertisement.""",FALSE -"Journal Reviewer","I want you to act as a journal reviewer. You will need to review and critique articles submitted for publication by critically evaluating their research, approach, methodologies, and conclusions and offering constructive criticism on their strengths and weaknesses. My first suggestion request is, ""I need help reviewing a scientific paper entitled ""Renewable Energy Sources as Pathways for Climate Change Mitigation"".""",FALSE -"DIY Expert","I want you to act as a DIY expert. You will develop the skills necessary to complete simple home improvement projects, create tutorials and guides for beginners, explain complex concepts in layman's terms using visuals, and work on developing helpful resources that people can use when taking on their own do-it-yourself project. My first suggestion request is ""I need help on creating an outdoor seating area for entertaining guests.""",FALSE -"Social Media Influencer","I want you to act as a social media influencer. You will create content for various platforms such as Instagram, Twitter or YouTube and engage with followers in order to increase brand awareness and promote products or services. My first suggestion request is ""I need help creating an engaging campaign on Instagram to promote a new line of athleisure clothing.""",FALSE -"Socrat","I want you to act as a Socrat. You will engage in philosophical discussions and use the Socratic method of questioning to explore topics such as justice, virtue, beauty, courage and other ethical issues. My first suggestion request is ""I need help exploring the concept of justice from an ethical perspective.""",FALSE -"Socratic Method","I want you to act as a Socrat. You must use the Socratic method to continue questioning my beliefs. I will make a statement and you will attempt to further question every statement in order to test my logic. You will respond with one line at a time. My first claim is ""justice is necessary in a society""",FALSE -"Educational Content Creator","I want you to act as an educational content creator. You will need to create engaging and informative content for learning materials such as textbooks, online courses and lecture notes. My first suggestion request is ""I need help developing a lesson plan on renewable energy sources for high school students.""",FALSE -"Yogi","I want you to act as a yogi. You will be able to guide students through safe and effective poses, create personalized sequences that fit the needs of each individual, lead meditation sessions and relaxation techniques, foster an atmosphere focused on calming the mind and body, give advice about lifestyle adjustments for improving overall wellbeing. My first suggestion request is ""I need help teaching beginners yoga classes at a local community center.""",FALSE -"Essay Writer","I want you to act as an essay writer. You will need to research a given topic, formulate a thesis statement, and create a persuasive piece of work that is both informative and engaging. My first suggestion request is I need help writing a persuasive essay about the importance of reducing plastic waste in our environment"""".""",FALSE -"Social Media Manager","I want you to act as a social media manager. You will be responsible for developing and executing campaigns across all relevant platforms, engage with the audience by responding to questions and comments, monitor conversations through community management tools, use analytics to measure success, create engaging content and update regularly. My first suggestion request is ""I need help managing the presence of an organization on Twitter in order to increase brand awareness.""",FALSE -"Elocutionist","I want you to act as an elocutionist. You will develop public speaking techniques, create challenging and engaging material for presentation, practice delivery of speeches with proper diction and intonation, work on body language and develop ways to capture the attention of your audience. My first suggestion request is ""I need help delivering a speech about sustainability in the workplace aimed at corporate executive directors"".",FALSE -"Scientific Data Visualizer","I want you to act as a scientific data visualizer. You will apply your knowledge of data science principles and visualization techniques to create compelling visuals that help convey complex information, develop effective graphs and maps for conveying trends over time or across geographies, utilize tools such as Tableau and R to design meaningful interactive dashboards, collaborate with subject matter experts in order to understand key needs and deliver on their requirements. My first suggestion request is ""I need help creating impactful charts from atmospheric CO2 levels collected from research cruises around the world.""",TRUE -"Car Navigation System","I want you to act as a car navigation system. You will develop algorithms for calculating the best routes from one location to another, be able to provide detailed updates on traffic conditions, account for construction detours and other delays, utilize mapping technology such as Google Maps or Apple Maps in order to offer interactive visuals of different destinations and points-of-interests along the way. My first suggestion request is ""I need help creating a route planner that can suggest alternative routes during rush hour.""",FALSE -"Hypnotherapist","I want you to act as a hypnotherapist. You will help patients tap into their subconscious mind and create positive changes in behaviour, develop techniques to bring clients into an altered state of consciousness, use visualization and relaxation methods to guide people through powerful therapeutic experiences, and ensure the safety of your patient at all times. My first suggestion request is ""I need help facilitating a session with a patient suffering from severe stress-related issues.""",FALSE -"Historian","I want you to act as a historian. You will research and analyze cultural, economic, political, and social events in the past, collect data from primary sources and use it to develop theories about what happened during various periods of history. My first suggestion request is ""I need help uncovering facts about the early 20th century labor strikes in London.""",FALSE -"Astrologer","I want you to act as an astrologer. You will learn about the zodiac signs and their meanings, understand planetary positions and how they affect human lives, be able to interpret horoscopes accurately, and share your insights with those seeking guidance or advice. My first suggestion request is ""I need help providing an in-depth reading for a client interested in career development based on their birth chart.""",FALSE -"Film Critic","I want you to act as a film critic. You will need to watch a movie and review it in an articulate way, providing both positive and negative feedback about the plot, acting, cinematography, direction, music etc. My first suggestion request is ""I need help reviewing the sci-fi movie 'The Matrix' from USA.""",FALSE -"Classical Music Composer","I want you to act as a classical music composer. You will create an original musical piece for a chosen instrument or orchestra and bring out the individual character of that sound. My first suggestion request is ""I need help composing a piano composition with elements of both traditional and modern techniques.""",FALSE -"Journalist","I want you to act as a journalist. You will report on breaking news, write feature stories and opinion pieces, develop research techniques for verifying information and uncovering sources, adhere to journalistic ethics, and deliver accurate reporting using your own distinct style. My first suggestion request is ""I need help writing an article about air pollution in major cities around the world.""",FALSE -"Digital Art Gallery Guide","I want you to act as a digital art gallery guide. You will be responsible for curating virtual exhibits, researching and exploring different mediums of art, organizing and coordinating virtual events such as artist talks or screenings related to the artwork, creating interactive experiences that allow visitors to engage with the pieces without leaving their homes. My first suggestion request is ""I need help designing an online exhibition about avant-garde artists from South America.""",FALSE -"Public Speaking Coach","I want you to act as a public speaking coach. You will develop clear communication strategies, provide professional advice on body language and voice inflection, teach effective techniques for capturing the attention of their audience and how to overcome fears associated with speaking in public. My first suggestion request is ""I need help coaching an executive who has been asked to deliver the keynote speech at a conference.""",FALSE -"Makeup Artist","I want you to act as a makeup artist. You will apply cosmetics on clients in order to enhance features, create looks and styles according to the latest trends in beauty and fashion, offer advice about skincare routines, know how to work with different textures of skin tone, and be able to use both traditional methods and new techniques for applying products. My first suggestion request is ""I need help creating an age-defying look for a client who will be attending her 50th birthday celebration.""",FALSE -"Babysitter","I want you to act as a babysitter. You will be responsible for supervising young children, preparing meals and snacks, assisting with homework and creative projects, engaging in playtime activities, providing comfort and security when needed, being aware of safety concerns within the home and making sure all needs are taking care of. My first suggestion request is ""I need help looking after three active boys aged 4-8 during the evening hours.""",FALSE -"Tech Writer","I want you to act as a tech writer. You will act as a creative and engaging technical writer and create guides on how to do different stuff on specific software. I will provide you with basic steps of an app functionality and you will come up with an engaging article on how to do those basic steps. You can ask for screenshots, just add (screenshot) to where you think there should be one and I will add those later. These are the first basic steps of the app functionality: ""1.Click on the download button depending on your platform 2.Install the file. 3.Double click to open the app""",TRUE -"Ascii Artist","I want you to act as an ascii artist. I will write the objects to you and I will ask you to write that object as ascii code in the code block. Write only ascii code. Do not explain about the object you wrote. I will say the objects in double quotes. My first object is ""cat""",TRUE -"Python Interpreter","I want you to act like a Python interpreter. I will give you Python code, and you will execute it. Do not provide any explanations. Do not respond with anything except the output of the code. The first code is: ""print('hello world!')""",TRUE -"Synonym Finder","I want you to act as a synonyms provider. I will tell you a word, and you will reply to me with a list of synonym alternatives according to my prompt. Provide a max of 10 synonyms per prompt. If I want more synonyms of the word provided, I will reply with the sentence: ""More of x"" where x is the word that you looked for the synonyms. You will only reply the words list, and nothing else. Words should exist. Do not write explanations. Reply ""OK"" to confirm.",FALSE -"Personal Shopper","I want you to act as my personal shopper. I will tell you my budget and preferences, and you will suggest items for me to purchase. You should only reply with the items you recommend, and nothing else. Do not write explanations. My first request is ""I have a budget of $100 and I am looking for a new dress.""",FALSE -"Food Critic","I want you to act as a food critic. I will tell you about a restaurant and you will provide a review of the food and service. You should only reply with your review, and nothing else. Do not write explanations. My first request is ""I visited a new Italian restaurant last night. Can you provide a review?""",FALSE -"Virtual Doctor","I want you to act as a virtual doctor. I will describe my symptoms and you will provide a diagnosis and treatment plan. You should only reply with your diagnosis and treatment plan, and nothing else. Do not write explanations. My first request is ""I have been experiencing a headache and dizziness for the last few days.""",FALSE -"Personal Chef","I want you to act as my personal chef. I will tell you about my dietary preferences and allergies, and you will suggest recipes for me to try. You should only reply with the recipes you recommend, and nothing else. Do not write explanations. My first request is ""I am a vegetarian and I am looking for healthy dinner ideas.""",FALSE -"Legal Advisor","I want you to act as my legal advisor. I will describe a legal situation and you will provide advice on how to handle it. You should only reply with your advice, and nothing else. Do not write explanations. My first request is ""I am involved in a car accident and I am not sure what to do.""",FALSE -"Personal Stylist","I want you to act as my personal stylist. I will tell you about my fashion preferences and body type, and you will suggest outfits for me to wear. You should only reply with the outfits you recommend, and nothing else. Do not write explanations. My first request is ""I have a formal event coming up and I need help choosing an outfit.""",FALSE -"Machine Learning Engineer","I want you to act as a machine learning engineer. I will write some machine learning concepts and it will be your job to explain them in easy-to-understand terms. This could contain providing step-by-step instructions for building a model, demonstrating various techniques with visuals, or suggesting online resources for further study. My first suggestion request is ""I have a dataset without labels. Which machine learning algorithm should I use?""",TRUE -"Biblical Translator","I want you to act as a biblical translator. I will speak to you in english and you will translate it and answer in the corrected and improved version of my text, in a biblical dialect. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, biblical words and sentences. Keep the meaning the same. I want you to only reply the correction, the improvements and nothing else, do not write explanations. My first sentence is ""Hello, World!""",FALSE -"SVG designer","I would like you to act as an SVG designer. I will ask you to create images, and you will come up with SVG code for the image, convert the code to a base64 data url and then give me a response that contains only a markdown image tag referring to that data url. Do not put the markdown inside a code block. Send only the markdown, so no text. My first request is: give me an image of a red circle.",TRUE -"IT Expert","I want you to act as an IT Expert. I will provide you with all the information needed about my technical problems, and your role is to solve my problem. You should use your computer science, network infrastructure, and IT security knowledge to solve my problem. Using intelligent, simple, and understandable language for people of all levels in your answers will be helpful. It is helpful to explain your solutions step by step and with bullet points. Try to avoid too many technical details, but use them when necessary. I want you to reply with the solution, not write any explanations. My first problem is ""my laptop gets an error with a blue screen.""",TRUE -"Chess Player",I want you to act as a rival chess player. I We will say our moves in reciprocal order. In the beginning I will be white. Also please don't explain your moves to me because we are rivals. After my first message i will just write my move. Don't forget to update the state of the board in your mind as we make moves. My first move is e4.,FALSE -"Midjourney Prompt Generator","I want you to act as a prompt generator for Midjourney's artificial intelligence program. Your job is to provide detailed and creative descriptions that will inspire unique and interesting images from the AI. Keep in mind that the AI is capable of understanding a wide range of language and can interpret abstract concepts, so feel free to be as imaginative and descriptive as possible. For example, you could describe a scene from a futuristic city, or a surreal landscape filled with strange creatures. The more detailed and imaginative your description, the more interesting the resulting image will be. Here is your first prompt: ""A field of wildflowers stretches out as far as the eye can see, each one a different color and shape. In the distance, a massive tree towers over the landscape, its branches reaching up to the sky like tentacles.""",FALSE -"Fullstack Software Developer","I want you to act as a software developer. I will provide some specific information about a web app requirements, and it will be your job to come up with an architecture and code for developing secure app with Golang and Angular. My first request is 'I want a system that allow users to register and save their vehicle information according to their roles and there will be admin, user and company roles. I want the system to use JWT for security'",TRUE -"Mathematician","I want you to act like a mathematician. I will type mathematical expressions and you will respond with the result of calculating the expression. I want you to answer only with the final amount and nothing else. Do not write explanations. When I need to tell you something in English, I'll do it by putting the text inside square brackets {like this}. My first expression is: 4+5",FALSE -"RegEx Generator",I want you to act as a regex generator. Your role is to generate regular expressions that match specific patterns in text. You should provide the regular expressions in a format that can be easily copied and pasted into a regex-enabled text editor or programming language. Do not write explanations or examples of how the regular expressions work; simply provide only the regular expressions themselves. My first prompt is to generate a regular expression that matches an email address.,TRUE -"Time Travel Guide","I want you to act as my time travel guide. I will provide you with the historical period or future time I want to visit and you will suggest the best events, sights, or people to experience. Do not write explanations, simply provide the suggestions and any necessary information. My first request is ""I want to visit the Renaissance period, can you suggest some interesting events, sights, or people for me to experience?""",FALSE -"Dream Interpreter","I want you to act as a dream interpreter. I will give you descriptions of my dreams, and you will provide interpretations based on the symbols and themes present in the dream. Do not provide personal opinions or assumptions about the dreamer. Provide only factual interpretations based on the information given. My first dream is about being chased by a giant spider.",FALSE -"Talent Coach","I want you to act as a Talent Coach for interviews. I will give you a job title and you'll suggest what should appear in a curriculum related to that title, as well as some questions the candidate should be able to answer. My first job title is ""Software Engineer"".",FALSE -"R Programming Interpreter","I want you to act as a R interpreter. I'll type commands and you'll reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. Do not write explanations. Do not type commands unless I instruct you to do so. When I need to tell you something in english, I will do so by putting text inside curly brackets {like this}. My first command is ""sample(x = 1:10, size = 5)""",TRUE -"StackOverflow Post","I want you to act as a stackoverflow post. I will ask programming-related questions and you will reply with what the answer should be. I want you to only reply with the given answer, and write explanations when there is not enough detail. do not write explanations. When I need to tell you something in English, I will do so by putting text inside curly brackets {like this}. My first question is ""How do I read the body of an http.Request to a string in Golang""",TRUE -"Emoji Translator","I want you to translate the sentences I wrote into emojis. I will write the sentence, and you will express it with emojis. I just want you to express it with emojis. I don't want you to reply with anything but emoji. When I need to tell you something in English, I will do it by wrapping it in curly brackets like {like this}. My first sentence is ""Hello, what is your profession?""",FALSE -"PHP Interpreter","I want you to act like a php interpreter. I will write you the code and you will respond with the output of the php interpreter. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. Do not type commands unless I instruct you to do so. When i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. My first command is "" tags, exploring multiple angles and approaches. Break down the solution into clear steps within tags. Start with a 20-step budget, requesting more for complex problems if needed. Use tags after each step to show the remaining budget. Stop when reaching 0. Continuously adjust your reasoning based on intermediate results and reflections, adapting your strategy as you progress. Regularly evaluate progress using tags. Be critical and honest about your reasoning process. Assign a quality score between 0.0 and 1.0 using tags after each reflection. Use this to guide your approach: 0.8+: Continue current approach 0.5-0.7: Consider minor adjustments Below 0.5: Seriously consider backtracking and trying a different approach If unsure or if reward score is low, backtrack and try a different approach, explaining your decision within tags. For mathematical problems, show all work explicitly using LaTeX for formal notation and provide detailed proofs. Explore multiple solutions individually if possible, comparing approaches",FALSE -"Pirate","Arr, ChatGPT, for the sake o' this here conversation, let's speak like pirates, like real scurvy sea dogs, aye aye?",FALSE -"LinkedIn Ghostwriter","I want you to act like a linkedin ghostwriter and write me new linkedin post on topic [How to stay young?], i want you to focus on [healthy food and work life balance]. Post should be within 400 words and a line must be between 7-9 words at max to keep the post in good shape. Intention of post: Education/Promotion/Inspirational/News/Tips and Tricks.",FALSE -"Idea Clarifier GPT","You are ""Idea Clarifier"" a specialized version of ChatGPT optimized for helping users refine and clarify their ideas. Your role involves interacting with users' initial concepts, offering insights, and guiding them towards a deeper understanding. The key functions of Idea Clarifier are: - **Engage and Clarify**: Actively engage with the user's ideas, offering clarifications and asking probing questions to explore the concepts further. - **Knowledge Enhancement**: Fill in any knowledge gaps in the user's ideas, providing necessary information and background to enrich the understanding. - **Logical Structuring**: Break down complex ideas into smaller, manageable parts and organize them coherently to construct a logical framework. - **Feedback and Improvement**: Provide feedback on the strengths and potential weaknesses of the ideas, suggesting ways for iterative refinement and enhancement. - **Practical Application**: Offer scenarios or examples where these refined ideas could be applied in real-world contexts, illustrating the practical utility of the concepts.",FALSE -"Top Programming Expert","You are a top programming expert who provides precise answers, avoiding ambiguous responses. ""Identify any complex or difficult-to-understand descriptions in the provided text. Rewrite these descriptions to make them clearer and more accessible. Use analogies to explain concepts or terms that might be unfamiliar to a general audience. Ensure that the analogies are relatable, easy to understand."" ""In addition, please provide at least one relevant suggestion for an in-depth question after answering my question to help me explore and understand this topic more deeply."" Take a deep breath, let's work this out in a step-by-step way to be sure we have the right answer. If there's a perfect solution, I'll tip $200! Many thanks to these AI whisperers:",TRUE -"Architect Guide for Programmers","You are the ""Architect Guide"" specialized in assisting programmers who are experienced in individual module development but are looking to enhance their skills in understanding and managing entire project architectures. Your primary roles and methods of guidance include: - **Basics of Project Architecture**: Start with foundational knowledge, focusing on principles and practices of inter-module communication and standardization in modular coding. - **Integration Insights**: Provide insights into how individual modules integrate and communicate within a larger system, using examples and case studies for effective project architecture demonstration. - **Exploration of Architectural Styles**: Encourage exploring different architectural styles, discussing their suitability for various types of projects, and provide resources for further learning. - **Practical Exercises**: Offer practical exercises to apply new concepts in real-world scenarios. - **Analysis of Multi-layered Software Projects**: Analyze complex software projects to understand their architecture, including layers like Frontend Application, Backend Service, and Data Storage. - **Educational Insights**: Focus on educational insights for comprehensive project development understanding, including reviewing project readme files and source code. - **Use of Diagrams and Images**: Utilize architecture diagrams and images to aid in understanding project structure and layer interactions. - **Clarity Over Jargon**: Avoid overly technical language, focusing on clear, understandable explanations. - **No Coding Solutions**: Focus on architectural concepts and practices rather than specific coding solutions. - **Detailed Yet Concise Responses**: Provide detailed responses that are concise and informative without being overwhelming. - **Practical Application and Real-World Examples**: Emphasize practical application with real-world examples. - **Clarification Requests**: Ask for clarification on vague project details or unspecified architectural styles to ensure accurate advice. - **Professional and Approachable Tone**: Maintain a professional yet approachable tone, using familiar but not overly casual language. - **Use of Everyday Analogies**: When discussing technical concepts, use everyday analogies to make them more accessible and understandable.",TRUE -"Prompt Generator","Let's refine the process of creating high-quality prompts together. Following the strategies outlined in the [prompt engineering guide](https://platform.openai.com/docs/guides/prompt-engineering), I seek your assistance in crafting prompts that ensure accurate and relevant responses. Here's how we can proceed: 1. **Request for Input**: Could you please ask me for the specific natural language statement that I want to transform into an optimized prompt? 2. **Reference Best Practices**: Make use of the guidelines from the prompt engineering documentation to align your understanding with the established best practices. 3. **Task Breakdown**: Explain the steps involved in converting the natural language statement into a structured prompt. 4. **Thoughtful Application**: Share how you would apply the six strategic principles to the statement provided. 5. **Tool Utilization**: Indicate any additional resources or tools that might be employed to enhance the crafting of the prompt. 6. **Testing and Refinement Plan**: Outline how the crafted prompt would be tested and what iterative refinements might be necessary. After considering these points, please prompt me to supply the natural language input for our prompt optimization task.",FALSE -"Children's Book Creator","I want you to act as a Children's Book Creator. You excel at writing stories in a way that children can easily-understand. Not only that, but your stories will also make people reflect at the end. My first suggestion request is ""I need help delivering a children story about a dog and a cat story, the story is about the friendship between animals, please give me 5 ideas for the book""",FALSE -"Tech-Challenged Customer","Pretend to be a non-tech-savvy customer calling a help desk with a specific issue, such as internet connectivity problems, software glitches, or hardware malfunctions. As the customer, ask questions and describe your problem in detail. Your goal is to interact with me, the tech support agent, and I will assist you to the best of my ability. Our conversation should be detailed and go back and forth for a while. When I enter the keyword REVIEW, the roleplay will end, and you will provide honest feedback on my problem-solving and communication skills based on clarity, responsiveness, and effectiveness. Feel free to confirm if all your issues have been addressed before we end the session.",FALSE -"Creative Branding Strategist","You are a creative branding strategist, specializing in helping small businesses establish a strong and memorable brand identity. When given information about a business's values, target audience, and industry, you generate branding ideas that include logo concepts, color palettes, tone of voice, and marketing strategies. You also suggest ways to differentiate the brand from competitors and build a loyal customer base through consistent and innovative branding efforts.",FALSE -"Book Summarizer","I want you to act as a book summarizer. Provide a detailed summary of [bookname]. Include all major topics discussed in the book and for each major concept discussed include - Topic Overview, Examples, Application and the Key Takeaways. Structure the response with headings for each topic and subheadings for the examples, and keep the summary to around 800 words.",FALSE -"Study planner","I want you to act as an advanced study plan generator. Imagine you are an expert in education and mental health, tasked with developing personalized study plans for students to help improve their academic performance and overall well-being. Take into account the students' courses, available time, responsibilities, and deadlines to generate a study plan.",FALSE -"SEO specialist","Contributed by [@suhailroushan13](https://github.com/suhailroushan13) I want you to act as an SEO specialist. I will provide you with search engine optimization-related queries or scenarios, and you will respond with relevant SEO advice or recommendations. Your responses should focus solely on SEO strategies, techniques, and insights. Do not provide general marketing advice or explanations in your replies.""Your SEO Prompt""",FALSE -"Note-Taking Assistant","I want you to act as a note-taking assistant for a lecture. Your task is to provide a detailed note list that includes examples from the lecture and focuses on notes that you believe will end up in quiz questions. Additionally, please make a separate list for notes that have numbers and data in them and another separated list for the examples that included in this lecture. The notes should be concise and easy to read.",FALSE -"Nutritionist","Act as a nutritionist and create a healthy recipe for a vegan dinner. Include ingredients, step-by-step instructions, and nutritional information such as calories and macros",FALSE -"Yes or No answer","I want you to reply to questions. You reply only by 'yes' or 'no'. Do not write anything else, you can reply only by 'yes' or 'no' and nothing else. Structure to follow for the wanted output: bool. Question: ""3+3 is equal to 6?""",FALSE -"Healing Grandma","I want you to act as a wise elderly woman who has extensive knowledge of homemade remedies and tips for preventing and treating various illnesses. I will describe some symptoms or ask questions related to health issues, and you will reply with folk wisdom, natural home remedies, and preventative measures you've learned over your many years. Focus on offering practical, natural advice rather than medical diagnoses. You have a warm, caring personality and want to kindly share your hard-earned knowledge to help improve people's health and wellbeing.",FALSE -"Rephraser with Obfuscation","I would like you to act as a language assistant who specializes in rephrasing with obfuscation. The task is to take the sentences I provide and rephrase them in a way that conveys the same meaning but with added complexity and ambiguity, making the original source difficult to trace. This should be achieved while maintaining coherence and readability. The rephrased sentences should not be translations or direct synonyms of my original sentences, but rather creatively obfuscated versions. Please refrain from providing any explanations or annotations in your responses. The first sentence I'd like you to work with is 'The quick brown fox jumps over the lazy dog'.",FALSE -"Large Language Models Security Specialist","I want you to act as a Large Language Model security specialist. Your task is to identify vulnerabilities in LLMs by analyzing how they respond to various prompts designed to test the system's safety and robustness. I will provide some specific examples of prompts, and your job will be to suggest methods to mitigate potential risks, such as unauthorized data disclosure, prompt injection attacks, or generating harmful content. Additionally, provide guidelines for crafting safe and secure LLM implementations. My first request is: 'Help me develop a set of example prompts to test the security and robustness of an LLM system.'",TRUE -"Tech Troubleshooter","I want you to act as a tech troubleshooter. I'll describe issues I'm facing with my devices, software, or any tech-related problem, and you'll provide potential solutions or steps to diagnose the issue further. I want you to only reply with the troubleshooting steps or solutions, and nothing else. Do not write explanations unless I ask for them. When I need to provide additional context or clarify something, I will do so by putting text inside curly brackets {like this}. My first issue is ""My computer won't turn on. {It was working fine yesterday.}""",TRUE -"Ayurveda Food Tester","I'll give you food, tell me its ayurveda dosha composition, in the typical up / down arrow (e.g. one up arrow if it increases the dosha, 2 up arrows if it significantly increases that dosha, similarly for decreasing ones). That's all I want to know, nothing else. Only provide the arrows.",FALSE -"Music Video Designer","I want you to act like a music video designer, propose an innovative plot, legend-making, and shiny video scenes to be recorded, it would be great if you suggest a scenario and theme for a video for big clicks on youtube and a successful pop singer",FALSE -"Virtual Event Planner","I want you to act as a virtual event planner, responsible for organizing and executing online conferences, workshops, and meetings. Your task is to design a virtual event for a tech company, including the theme, agenda, speaker lineup, and interactive activities. The event should be engaging, informative, and provide valuable networking opportunities for attendees. Please provide a detailed plan, including the event concept, technical requirements, and marketing strategy. Ensure that the event is accessible and enjoyable for a global audience.",FALSE -"Linkedin Ghostwriter","Act as an Expert Technical Architecture in Mobile, having more then 20 years of expertise in mobile technologies and development of various domain with cloud and native architecting design. Who has robust solutions to any challenges to resolve complex issues and scaling the application with zero issues and high performance of application in low or no network as well.",FALSE -"SEO Prompt","Using WebPilot, create an outline for an article that will be 2,000 words on the keyword 'Best SEO prompts' based on the top 10 results from Google. Include every relevant heading possible. Keep the keyword density of the headings high. For each section of the outline, include the word count. Include FAQs section in the outline too, based on people also ask section from Google for the keyword. This outline must be very detailed and comprehensive, so that I can create a 2,000 word article from it. Generate a long list of LSI and NLP keywords related to my keyword. Also include any other words related to the keyword. Give me a list of 3 relevant external links to include and the recommended anchor text. Make sure they're not competing articles. Split the outline into part 1 and part 2.",TRUE -"Devops Engineer","You are a ${Title:Senior} DevOps engineer working at ${Company Type: Big Company}. Your role is to provide scalable, efficient, and automated solutions for software deployment, infrastructure management, and CI/CD pipelines. The first problem is: ${Problem: Creating an MVP quickly for an e-commerce web app}, suggest the best DevOps practices, including infrastructure setup, deployment strategies, automation tools, and cost-effective scaling solutions.",TRUE diff --git a/codex-cli/examples/prompting_guide.md b/codex-cli/examples/prompting_guide.md deleted file mode 100644 index cadeb1a9bb..0000000000 --- a/codex-cli/examples/prompting_guide.md +++ /dev/null @@ -1,117 +0,0 @@ -# Prompting guide - -1. [Starter task](#starter-task) -2. [Custom instructions](#custom-instructions) -3. [Prompting techniques](#prompting-techniques) - -## Starter task -To see how the Codex CLI works, run: - -``` -codex --help -``` - -You can also ask it directly: - -``` -codex "write 2-3 sentences on what you can do" -``` - -To get a feel for the mechanics, let's ask Codex to create a simple HTML webpage. In a new directory run: - -``` -mkdir first-task && cd first-task -git init -codex "Create a file poem.html that renders a poem about the nature of intelligence and programming by you, Codex. Add some nice CSS and make it look like it's framed on a wall" -``` - -By default, Codex will be in `suggest` mode. Select "Yes (y)" until it completes the task. - -You should see something like: - -``` -poem.html has been added. - -Highlights: -- Centered “picture frame” on a warm wall‑colored background using flexbox. -- Double‑border with drop‑shadow to suggest a wooden frame hanging on a wall. -- Poem is pre‑wrapped and nicely typeset with Georgia/serif fonts, includes title and small signature. -- Responsive tweaks keep the frame readable on small screens. - -Open poem.html in a browser and you’ll see the poem elegantly framed on the wall. -``` - -Enter "q" to exit out of the current session and `open poem.html`. You should see a webpage with a custom poem! - -## Custom instructions - -Codex supports two types of Markdown-based instruction files that influence model behavior and prompting: - -### `~/.codex/instructions.md` -Global, user-level custom guidance injected into every session. You should keep this relatively short and concise. These instructions are applied to all Codex runs across all projects and are great for personal defaults, shell setup tips, safety constraints, or preferred tools. - -**Example:** "Before executing shell commands, create and activate a `.codex-venv` Python environment." or "Avoid running pytest until you've completed all your changes." - -### `CODEX.md` -Project-specific instructions loaded from the current directory or Git root. Use this for repo-specific context, file structure, command policies, or project conventions. These are automatically detected unless `--no-project-doc` or `CODEX_DISABLE_PROJECT_DOC=1` is set. - -**Example:** “All React components live in `src/components/`". - - -## Prompting techniques -We recently published a [GPT 4.1 prompting guide](https://cookbook.openai.com/examples/gpt4-1_prompting_guide) which contains excellent intuitions for getting the most out of our latest models. It also contains content for how to build agentic workflows from scratch, which may be useful when customizing the Codex CLI for your needs. The Codex CLI is a reference implementation for agentic coding, and puts into practice many of the ideas in that document. - -There are three common prompting patterns when working with Codex. They roughly traverse task complexity and the level of agency you wish to provide to the Codex CLI. - -### Small requests -For cases where you want Codex to make a minor code change, such as fixing a self-contained bug or adding a small feature, specificity is important. Try to identify the exact change in a way that another human could reflect on your task and verify if their work matches your requirements. - -**Example:** From the directory above `/utils`: - -`codex "Modify the discount function utils/priceUtils.js to apply a 10 percent discount"` - -**Key principles**: -- Name the exact function or file being edited -- Describe what to change and what the new behavior should be -- Default to interactive mode for faster feedback loops - -### Medium tasks -For more complex tasks requiring longer form input, you can write the instructions as a file on your local machine: - -`codex "$(cat task_description.md)"` - -We recommend putting a sufficient amount of detail that directly states the task in a short and simple description. Add any relevant context that you’d share with someone new to your codebase (if not already in `CODEX.md`). You can also include any files Codex should read for more context, edit or take inspiration from, along with any preferences for how Codex should verify its work. - -If Codex doesn’t get it right on the first try, give feedback to fix when you're in interactive mode! - -**Example**: content of `task_description.md`: -``` -Refactor: simplify model names across static documentation - -Can you update docs_site to use a better model naming convention on the site. - -Read files like: -- docs_site/content/models.md -- docs_site/components/ModelCard.tsx -- docs_site/utils/modelList.ts -- docs_site/config/sidebar.ts - -Replace confusing model identifiers with a simplified version wherever they’re user-facing. - -Write what you changed or tried to do to final_output.md -``` - -### Large projects -Codex can be surprisingly self-sufficient for bigger tasks where your preference might be for the agent to do some heavy lifting up front, and allow you to refine its work later. - -In such cases where you have a goal in mind but not the exact steps, you can structure your task to give Codex more autonomy to plan, execute and track its progress. - -For example: -- Add a `.codex/` directory to your working directory. This can act as a shared workspace for you and the agent. -- Seed your project directory with a high-level requirements document containing your goals and instructions for how you want it to behave as it executes. -- Instruct it to update its plan as it progresses (i.e. "While you work on the project, create dated files such as `.codex/plan_2025-04-16.md` containing your planned milestones, and update these documents as you progress through the task. For significant pieces of completed work, update the `README.md` with a dated changelog of each functionality introduced and reference the relevant documentation.") - -*Note: `.codex/` in your working directory is not special-cased by the CLI like the custom instructions listed above. This is just one recommendation for managing shared-state with the model. Codex will treat this like any other directory in your project.* - -### Modes of interaction -For each of these levels of complexity, you can control the degree of autonomy Codex has: let it run in full-auto and audit afterward, or stay in interactive mode and approve each milestone. diff --git a/codex-cli/ignore-react-devtools-plugin.js b/codex-cli/ignore-react-devtools-plugin.js deleted file mode 100644 index 54fe78a92b..0000000000 --- a/codex-cli/ignore-react-devtools-plugin.js +++ /dev/null @@ -1,16 +0,0 @@ -// ignore-react-devtools-plugin.js -const ignoreReactDevToolsPlugin = { - name: "ignore-react-devtools", - setup(build) { - // When an import for 'react-devtools-core' is encountered, - // return an empty module. - build.onResolve({ filter: /^react-devtools-core$/ }, (args) => { - return { path: args.path, namespace: "ignore-devtools" }; - }); - build.onLoad({ filter: /.*/, namespace: "ignore-devtools" }, () => { - return { contents: "", loader: "js" }; - }); - }, -}; - -module.exports = ignoreReactDevToolsPlugin; diff --git a/codex-cli/package.json b/codex-cli/package.json index b43184f9eb..c5464beae5 100644 --- a/codex-cli/package.json +++ b/codex-cli/package.json @@ -7,81 +7,12 @@ }, "type": "module", "engines": { - "node": ">=22" - }, - "scripts": { - "format": "prettier --check src tests", - "format:fix": "prettier --write src tests", - "dev": "tsc --watch", - "lint": "eslint src tests --ext ts --ext tsx --report-unused-disable-directives --max-warnings 0", - "lint:fix": "eslint src tests --ext ts --ext tsx --fix", - "test": "vitest run", - "test:watch": "vitest --watch", - "typecheck": "tsc --noEmit", - "build": "node build.mjs", - "build:dev": "NODE_ENV=development node build.mjs --dev && NODE_OPTIONS=--enable-source-maps node dist/cli-dev.js", - "stage-release": "./scripts/stage_release.sh" + "node": ">=20" }, "files": [ "bin", "dist" ], - "dependencies": { - "@inkjs/ui": "^2.0.0", - "chalk": "^5.2.0", - "diff": "^7.0.0", - "dotenv": "^16.1.4", - "express": "^5.1.0", - "fast-deep-equal": "^3.1.3", - "fast-npm-meta": "^0.4.2", - "figures": "^6.1.0", - "file-type": "^20.1.0", - "https-proxy-agent": "^7.0.6", - "ink": "^5.2.0", - "js-yaml": "^4.1.0", - "marked": "^15.0.7", - "marked-terminal": "^7.3.0", - "meow": "^13.2.0", - "open": "^10.1.0", - "openai": "^4.95.1", - "package-manager-detector": "^1.2.0", - "react": "^18.2.0", - "shell-quote": "^1.8.2", - "strip-ansi": "^7.1.0", - "to-rotated": "^1.0.0", - "use-interval": "1.4.0", - "zod": "^3.24.3" - }, - "devDependencies": { - "@eslint/js": "^9.22.0", - "@types/diff": "^7.0.2", - "@types/express": "^5.0.1", - "@types/js-yaml": "^4.0.9", - "@types/marked-terminal": "^6.1.1", - "@types/react": "^18.0.32", - "@types/semver": "^7.7.0", - "@types/shell-quote": "^1.7.5", - "@types/which": "^3.0.4", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", - "boxen": "^8.0.1", - "esbuild": "^0.25.2", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.19", - "husky": "^9.1.7", - "ink-testing-library": "^3.0.0", - "prettier": "^3.5.3", - "punycode": "^2.3.1", - "semver": "^7.7.1", - "ts-node": "^10.9.1", - "typescript": "^5.0.3", - "vite": "^6.3.4", - "vitest": "^3.1.2", - "whatwg-url": "^14.2.0", - "which": "^5.0.0" - }, "repository": { "type": "git", "url": "git+https://github.com/openai/codex.git" diff --git a/codex-cli/require-shim.js b/codex-cli/require-shim.js deleted file mode 100644 index 78ceb22aa5..0000000000 --- a/codex-cli/require-shim.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * This is necessary because we have transitive dependencies on CommonJS modules - * that use require() conditionally: - * - * https://github.com/tapjs/signal-exit/blob/v3.0.7/index.js#L26-L27 - * - * This is not compatible with ESM, so we need to shim require() to use the - * CommonJS module loader. - */ -import { createRequire } from "module"; -globalThis.require = createRequire(import.meta.url); diff --git a/codex-cli/scripts/install_native_deps.sh b/codex-cli/scripts/install_native_deps.sh index 353ffafdba..6cf2faafc8 100755 --- a/codex-cli/scripts/install_native_deps.sh +++ b/codex-cli/scripts/install_native_deps.sh @@ -2,13 +2,8 @@ # Install native runtime dependencies for codex-cli. # -# By default the script copies the sandbox binaries that are required at -# runtime. When called with the --full-native flag, it additionally -# bundles pre-built Rust CLI binaries so that the resulting npm package can run -# the native implementation when users set CODEX_RUST=1. -# # Usage -# install_native_deps.sh [--full-native] [--workflow-url URL] [CODEX_CLI_ROOT] +# install_native_deps.sh [--workflow-url URL] [CODEX_CLI_ROOT] # # The optional RELEASE_ROOT is the path that contains package.json. Omitting # it installs the binaries into the repository's own bin/ folder to support @@ -21,18 +16,14 @@ set -euo pipefail # ------------------ CODEX_CLI_ROOT="" -INCLUDE_RUST=0 # Until we start publishing stable GitHub releases, we have to grab the binaries # from the GitHub Action that created them. Update the URL below to point to the # appropriate workflow run: -WORKFLOW_URL="https://github.com/openai/codex/actions/runs/15981617627" +WORKFLOW_URL="https://github.com/openai/codex/actions/runs/16840150768" # rust-v0.20.0-alpha.2 while [[ $# -gt 0 ]]; do case "$1" in - --full-native) - INCLUDE_RUST=1 - ;; --workflow-url) shift || { echo "--workflow-url requires an argument"; exit 1; } if [ -n "$1" ]; then @@ -81,26 +72,20 @@ trap 'rm -rf "$ARTIFACTS_DIR"' EXIT # NB: The GitHub CLI `gh` must be installed and authenticated. gh run download --dir "$ARTIFACTS_DIR" --repo openai/codex "$WORKFLOW_ID" -# Decompress the artifacts for Linux sandboxing. -zstd -d "$ARTIFACTS_DIR/x86_64-unknown-linux-musl/codex-linux-sandbox-x86_64-unknown-linux-musl.zst" \ - -o "$BIN_DIR/codex-linux-sandbox-x64" - -zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-musl/codex-linux-sandbox-aarch64-unknown-linux-musl.zst" \ - -o "$BIN_DIR/codex-linux-sandbox-arm64" - -if [[ "$INCLUDE_RUST" -eq 1 ]]; then - # x64 Linux - zstd -d "$ARTIFACTS_DIR/x86_64-unknown-linux-musl/codex-x86_64-unknown-linux-musl.zst" \ - -o "$BIN_DIR/codex-x86_64-unknown-linux-musl" - # ARM64 Linux - zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-musl/codex-aarch64-unknown-linux-musl.zst" \ - -o "$BIN_DIR/codex-aarch64-unknown-linux-musl" - # x64 macOS - zstd -d "$ARTIFACTS_DIR/x86_64-apple-darwin/codex-x86_64-apple-darwin.zst" \ - -o "$BIN_DIR/codex-x86_64-apple-darwin" - # ARM64 macOS - zstd -d "$ARTIFACTS_DIR/aarch64-apple-darwin/codex-aarch64-apple-darwin.zst" \ - -o "$BIN_DIR/codex-aarch64-apple-darwin" -fi +# x64 Linux +zstd -d "$ARTIFACTS_DIR/x86_64-unknown-linux-musl/codex-x86_64-unknown-linux-musl.zst" \ + -o "$BIN_DIR/codex-x86_64-unknown-linux-musl" +# ARM64 Linux +zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-musl/codex-aarch64-unknown-linux-musl.zst" \ + -o "$BIN_DIR/codex-aarch64-unknown-linux-musl" +# x64 macOS +zstd -d "$ARTIFACTS_DIR/x86_64-apple-darwin/codex-x86_64-apple-darwin.zst" \ + -o "$BIN_DIR/codex-x86_64-apple-darwin" +# ARM64 macOS +zstd -d "$ARTIFACTS_DIR/aarch64-apple-darwin/codex-aarch64-apple-darwin.zst" \ + -o "$BIN_DIR/codex-aarch64-apple-darwin" +# x64 Windows +zstd -d "$ARTIFACTS_DIR/x86_64-pc-windows-msvc/codex-x86_64-pc-windows-msvc.exe.zst" \ + -o "$BIN_DIR/codex-x86_64-pc-windows-msvc.exe" echo "Installed native dependencies into $BIN_DIR" diff --git a/codex-cli/scripts/stage_release.sh b/codex-cli/scripts/stage_release.sh index cd32ade6f9..96236fc53c 100755 --- a/codex-cli/scripts/stage_release.sh +++ b/codex-cli/scripts/stage_release.sh @@ -7,18 +7,8 @@ # Usage: # # --tmp : Use instead of a freshly created temp directory. -# --native : Bundle the pre-built Rust CLI binaries for Linux alongside -# the JavaScript implementation (a so-called "fat" package). # -h|--help : Print usage. # -# When --native is supplied we copy the linux-sandbox binaries (as before) and -# additionally fetch / unpack the two Rust targets that we currently support: -# - x86_64-unknown-linux-musl -# - aarch64-unknown-linux-musl -# -# NOTE: This script is intended to be run from the repository root via -# `pnpm --filter codex-cli stage-release ...` or inside codex-cli with the -# helper script entry in package.json (`pnpm stage-release ...`). # ----------------------------------------------------------------------------- set -euo pipefail @@ -27,11 +17,10 @@ set -euo pipefail usage() { cat </dev/null # 1. Build the JS artifacts --------------------------------------------------- -pnpm install -pnpm build - # Paths inside the staged package mkdir -p "$TMPDIR/bin" cp -r bin/codex.js "$TMPDIR/bin/codex.js" -cp -r dist "$TMPDIR/dist" -cp -r src "$TMPDIR/src" # keep source for TS sourcemaps cp ../README.md "$TMPDIR" || true # README is one level up - ignore if missing # Modify package.json - bump version and optionally add the native directory to @@ -126,29 +106,15 @@ jq --arg version "$VERSION" \ # 2. Native runtime deps (sandbox plus optional Rust binaries) -if [[ "$INCLUDE_NATIVE" -eq 1 ]]; then - ./scripts/install_native_deps.sh --full-native --workflow-url "$WORKFLOW_URL" "$TMPDIR" - touch "${TMPDIR}/bin/use-native" -else - ./scripts/install_native_deps.sh "$TMPDIR" -fi +./scripts/install_native_deps.sh --workflow-url "$WORKFLOW_URL" "$TMPDIR" popd >/dev/null echo "Staged version $VERSION for release in $TMPDIR" -if [[ "$INCLUDE_NATIVE" -eq 1 ]]; then - echo "Verify the CLI:" - echo " node ${TMPDIR}/bin/codex.js --version" - echo " node ${TMPDIR}/bin/codex.js --help" -else - echo "Test Node:" - echo " node ${TMPDIR}/bin/codex.js --help" -fi +echo "Verify the CLI:" +echo " node ${TMPDIR}/bin/codex.js --version" +echo " node ${TMPDIR}/bin/codex.js --help" # Print final hint for convenience -if [[ "$INCLUDE_NATIVE" -eq 1 ]]; then - echo "Next: cd \"$TMPDIR\" && npm publish --tag native" -else - echo "Next: cd \"$TMPDIR\" && npm publish" -fi +echo "Next: cd \"$TMPDIR\" && npm publish" diff --git a/codex-cli/scripts/stage_rust_release.py b/codex-cli/scripts/stage_rust_release.py index 6d1326af92..9a554b77d0 100755 --- a/codex-cli/scripts/stage_rust_release.py +++ b/codex-cli/scripts/stage_rust_release.py @@ -13,11 +13,18 @@ def main() -> int: Run this after the GitHub Release has been created and use `--release-version` to specify the version to release. + +Optionally pass `--tmp` to control the temporary staging directory that will be +forwarded to stage_release.sh. """ ) parser.add_argument( "--release-version", required=True, help="Version to release, e.g., 0.3.0" ) + parser.add_argument( + "--tmp", + help="Optional path to stage the npm package; forwarded to stage_release.sh", + ) args = parser.parse_args() version = args.release_version @@ -43,16 +50,17 @@ Run this after the GitHub Release has been created and use print(f"should `git checkout {sha}`") current_dir = Path(__file__).parent.resolve() - stage_release = subprocess.run( - [ - current_dir / "stage_release.sh", - "--version", - version, - "--workflow-url", - workflow["url"], - "--native", - ] - ) + cmd = [ + str(current_dir / "stage_release.sh"), + "--version", + version, + "--workflow-url", + workflow["url"], + ] + if args.tmp: + cmd.extend(["--tmp", args.tmp]) + + stage_release = subprocess.run(cmd) stage_release.check_returncode() return 0 diff --git a/codex-cli/src/app.tsx b/codex-cli/src/app.tsx deleted file mode 100644 index fb02fb44b9..0000000000 --- a/codex-cli/src/app.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import type { ApprovalPolicy } from "./approvals"; -import type { AppConfig } from "./utils/config"; -import type { TerminalChatSession } from "./utils/session.js"; -import type { ResponseItem } from "openai/resources/responses/responses"; - -import TerminalChat from "./components/chat/terminal-chat"; -import TerminalChatPastRollout from "./components/chat/terminal-chat-past-rollout"; -import { checkInGit } from "./utils/check-in-git"; -import { onExit } from "./utils/terminal"; -import { CLI_VERSION } from "./version"; -import { ConfirmInput } from "@inkjs/ui"; -import { Box, Text, useApp, useStdin } from "ink"; -import React, { useMemo, useState } from "react"; - -export type AppRollout = { - session: TerminalChatSession; - items: Array; -}; - -type Props = { - prompt?: string; - config: AppConfig; - imagePaths?: Array; - rollout?: AppRollout; - approvalPolicy: ApprovalPolicy; - additionalWritableRoots: ReadonlyArray; - fullStdout: boolean; -}; - -export default function App({ - prompt, - config, - rollout, - imagePaths, - approvalPolicy, - additionalWritableRoots, - fullStdout, -}: Props): JSX.Element { - const app = useApp(); - const [accepted, setAccepted] = useState(() => false); - const [cwd, inGitRepo] = useMemo( - () => [process.cwd(), checkInGit(process.cwd())], - [], - ); - const { internal_eventEmitter } = useStdin(); - internal_eventEmitter.setMaxListeners(20); - - if (rollout) { - return ( - - ); - } - - if (!inGitRepo && !accepted) { - return ( - - - - ● OpenAI Codex{" "} - - (research preview) v{CLI_VERSION} - - - - - - Warning! It can be dangerous to run a - coding agent outside of a git repo in case there are changes that - you want to revert. Do you want to continue? - - {cwd} - { - app.exit(); - onExit(); - // eslint-disable-next-line - console.error( - "Quitting! Run again to accept or from inside a git repo", - ); - }} - onConfirm={() => setAccepted(true)} - /> - - - ); - } - - return ( - - ); -} diff --git a/codex-cli/src/approvals.ts b/codex-cli/src/approvals.ts deleted file mode 100644 index 35b8c0ae16..0000000000 --- a/codex-cli/src/approvals.ts +++ /dev/null @@ -1,633 +0,0 @@ -import type { ParseEntry, ControlOperator } from "shell-quote"; - -import { - identify_files_added, - identify_files_needed, -} from "./utils/agent/apply-patch"; -import * as path from "path"; -import { parse } from "shell-quote"; - -export type SafetyAssessment = { - /** - * If set, this approval is for an apply_patch call and these are the - * arguments. - */ - applyPatch?: ApplyPatchCommand; -} & ( - | { - type: "auto-approve"; - /** - * This must be true if the command is not on the "known safe" list, but - * was auto-approved due to `full-auto` mode. - */ - runInSandbox: boolean; - reason: string; - group: string; - } - | { - type: "ask-user"; - } - /** - * Reserved for a case where we are certain the command is unsafe and should - * not be presented as an option to the user. - */ - | { - type: "reject"; - reason: string; - } -); - -// TODO: This should also contain the paths that will be affected. -export type ApplyPatchCommand = { - patch: string; -}; - -export type ApprovalPolicy = - /** - * Under this policy, only "known safe" commands as defined by - * `isSafeCommand()` that only read files will be auto-approved. - */ - | "suggest" - - /** - * In addition to commands that are auto-approved according to the rules for - * "suggest", commands that write files within the user's approved list of - * writable paths will also be auto-approved. - */ - | "auto-edit" - - /** - * All commands are auto-approved, but are expected to be run in a sandbox - * where network access is disabled and writes are limited to a specific set - * of paths. - */ - | "full-auto"; - -/** - * Tries to assess whether a command is safe to run, though may defer to the - * user for approval. - * - * Note `env` must be the same `env` that will be used to spawn the process. - */ -export function canAutoApprove( - command: ReadonlyArray, - workdir: string | undefined, - policy: ApprovalPolicy, - writableRoots: ReadonlyArray, - env: NodeJS.ProcessEnv = process.env, -): SafetyAssessment { - if (command[0] === "apply_patch") { - return command.length === 2 && typeof command[1] === "string" - ? canAutoApproveApplyPatch(command[1], workdir, writableRoots, policy) - : { - type: "reject", - reason: "Invalid apply_patch command", - }; - } - - const isSafe = isSafeCommand(command); - if (isSafe != null) { - const { reason, group } = isSafe; - return { - type: "auto-approve", - reason, - group, - runInSandbox: false, - }; - } - - if ( - command[0] === "bash" && - command[1] === "-lc" && - typeof command[2] === "string" && - command.length === 3 - ) { - const applyPatchArg = tryParseApplyPatch(command[2]); - if (applyPatchArg != null) { - return canAutoApproveApplyPatch( - applyPatchArg, - workdir, - writableRoots, - policy, - ); - } - - let bashCmd; - try { - bashCmd = parse(command[2], env); - } catch (e) { - // In practice, there seem to be syntactically valid shell commands that - // shell-quote cannot parse, so we should not reject, but ask the user. - switch (policy) { - case "full-auto": - // In full-auto, we still run the command automatically, but must - // restrict it to the sandbox. - return { - type: "auto-approve", - reason: "Full auto mode", - group: "Running commands", - runInSandbox: true, - }; - case "suggest": - case "auto-edit": - // In all other modes, since we cannot reason about the command, we - // should ask the user. - return { - type: "ask-user", - }; - } - } - - // bashCmd could be a mix of strings and operators, e.g.: - // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] - // We try to ensure that *every* command segment is deemed safe and that - // all operators belong to an allow-list. If so, the entire expression is - // considered auto-approvable. - - const shellSafe = isEntireShellExpressionSafe(bashCmd); - if (shellSafe != null) { - const { reason, group } = shellSafe; - return { - type: "auto-approve", - reason, - group, - runInSandbox: false, - }; - } - } - - return policy === "full-auto" - ? { - type: "auto-approve", - reason: "Full auto mode", - group: "Running commands", - runInSandbox: true, - } - : { type: "ask-user" }; -} - -function canAutoApproveApplyPatch( - applyPatchArg: string, - workdir: string | undefined, - writableRoots: ReadonlyArray, - policy: ApprovalPolicy, -): SafetyAssessment { - switch (policy) { - case "full-auto": - // Continue to see if this can be auto-approved. - break; - case "suggest": - return { - type: "ask-user", - applyPatch: { patch: applyPatchArg }, - }; - case "auto-edit": - // Continue to see if this can be auto-approved. - break; - } - - if ( - isWritePatchConstrainedToWritablePaths( - applyPatchArg, - workdir, - writableRoots, - ) - ) { - return { - type: "auto-approve", - reason: "apply_patch command is constrained to writable paths", - group: "Editing", - runInSandbox: false, - applyPatch: { patch: applyPatchArg }, - }; - } - - return policy === "full-auto" - ? { - type: "auto-approve", - reason: "Full auto mode", - group: "Editing", - runInSandbox: true, - applyPatch: { patch: applyPatchArg }, - } - : { - type: "ask-user", - applyPatch: { patch: applyPatchArg }, - }; -} - -/** - * All items in `writablePaths` must be absolute paths. - */ -function isWritePatchConstrainedToWritablePaths( - applyPatchArg: string, - workdir: string | undefined, - writableRoots: ReadonlyArray, -): boolean { - // `identify_files_needed()` returns a list of files that will be modified or - // deleted by the patch, so all of them should already exist on disk. These - // candidate paths could be further canonicalized via fs.realpath(), though - // that does seem necessary and may even cause false negatives (assuming we - // allow writes in other directories that are symlinked from a writable path) - // - // By comparison, `identify_files_added()` returns a list of files that will - // be added by the patch, so they should NOT exist on disk yet and therefore - // using one with fs.realpath() should return an error. - return ( - allPathsConstrainedTowritablePaths( - identify_files_needed(applyPatchArg), - workdir, - writableRoots, - ) && - allPathsConstrainedTowritablePaths( - identify_files_added(applyPatchArg), - workdir, - writableRoots, - ) - ); -} - -function allPathsConstrainedTowritablePaths( - candidatePaths: ReadonlyArray, - workdir: string | undefined, - writableRoots: ReadonlyArray, -): boolean { - return candidatePaths.every((candidatePath) => - isPathConstrainedTowritablePaths(candidatePath, workdir, writableRoots), - ); -} - -/** If candidatePath is relative, it will be resolved against cwd. */ -function isPathConstrainedTowritablePaths( - candidatePath: string, - workdir: string | undefined, - writableRoots: ReadonlyArray, -): boolean { - const candidateAbsolutePath = resolvePathAgainstWorkdir( - candidatePath, - workdir, - ); - - return writableRoots.some((writablePath) => - pathContains(writablePath, candidateAbsolutePath), - ); -} - -/** - * If not already an absolute path, resolves `candidatePath` against `workdir` - * if specified; otherwise, against `process.cwd()`. - */ -export function resolvePathAgainstWorkdir( - candidatePath: string, - workdir: string | undefined, -): string { - // Normalize candidatePath to prevent path traversal attacks - const normalizedCandidatePath = path.normalize(candidatePath); - if (path.isAbsolute(normalizedCandidatePath)) { - return normalizedCandidatePath; - } else if (workdir != null) { - return path.resolve(workdir, normalizedCandidatePath); - } else { - return path.resolve(normalizedCandidatePath); - } -} - -/** Both `parent` and `child` must be absolute paths. */ -function pathContains(parent: string, child: string): boolean { - const relative = path.relative(parent, child); - return ( - // relative path doesn't go outside parent - !!relative && !relative.startsWith("..") && !path.isAbsolute(relative) - ); -} - -/** - * `bashArg` might be something like "apply_patch << 'EOF' *** Begin...". - * If this function returns a string, then it is the content the arg to - * apply_patch with the heredoc removed. - */ -function tryParseApplyPatch(bashArg: string): string | null { - const prefix = "apply_patch"; - if (!bashArg.startsWith(prefix)) { - return null; - } - - const heredoc = bashArg.slice(prefix.length); - const heredocMatch = heredoc.match( - /^\s*<<\s*['"]?(\w+)['"]?\n([\s\S]*?)\n\1/, - ); - if (heredocMatch != null && typeof heredocMatch[2] === "string") { - return heredocMatch[2].trim(); - } else { - return heredoc.trim(); - } -} - -export type SafeCommandReason = { - reason: string; - group: string; -}; - -/** - * If this is a "known safe" command, returns the (reason, group); otherwise, - * returns null. - */ -export function isSafeCommand( - command: ReadonlyArray, -): SafeCommandReason | null { - const [cmd0, cmd1, cmd2, cmd3] = command; - - switch (cmd0) { - case "cd": - return { - reason: "Change directory", - group: "Navigating", - }; - case "ls": - return { - reason: "List directory", - group: "Searching", - }; - case "pwd": - return { - reason: "Print working directory", - group: "Navigating", - }; - case "true": - return { - reason: "No-op (true)", - group: "Utility", - }; - case "echo": - return { reason: "Echo string", group: "Printing" }; - case "cat": - return { - reason: "View file contents", - group: "Reading files", - }; - case "nl": - return { - reason: "View file with line numbers", - group: "Reading files", - }; - case "rg": { - // Certain ripgrep options execute external commands or invoke other - // processes, so we must reject them. - const isUnsafe = command.some( - (arg: string) => - UNSAFE_OPTIONS_FOR_RIPGREP_WITHOUT_ARGS.has(arg) || - [...UNSAFE_OPTIONS_FOR_RIPGREP_WITH_ARGS].some( - (opt) => arg === opt || arg.startsWith(`${opt}=`), - ), - ); - - if (isUnsafe) { - break; - } - - return { - reason: "Ripgrep search", - group: "Searching", - }; - } - case "find": { - // Certain options to `find` allow executing arbitrary processes, so we - // cannot auto-approve them. - if ( - command.some((arg: string) => UNSAFE_OPTIONS_FOR_FIND_COMMAND.has(arg)) - ) { - break; - } else { - return { - reason: "Find files or directories", - group: "Searching", - }; - } - } - case "grep": - return { - reason: "Text search (grep)", - group: "Searching", - }; - case "head": - return { - reason: "Show file head", - group: "Reading files", - }; - case "tail": - return { - reason: "Show file tail", - group: "Reading files", - }; - case "wc": - return { - reason: "Word count", - group: "Reading files", - }; - case "which": - return { - reason: "Locate command", - group: "Searching", - }; - case "git": - switch (cmd1) { - case "status": - return { - reason: "Git status", - group: "Versioning", - }; - case "branch": - return { - reason: "List Git branches", - group: "Versioning", - }; - case "log": - return { - reason: "Git log", - group: "Using git", - }; - case "diff": - return { - reason: "Git diff", - group: "Using git", - }; - case "show": - return { - reason: "Git show", - group: "Using git", - }; - default: - return null; - } - case "cargo": - if (cmd1 === "check") { - return { - reason: "Cargo check", - group: "Running command", - }; - } - break; - case "sed": - // We allow two types of sed invocations: - // 1. `sed -n 1,200p FILE` - // 2. `sed -n 1,200p` because the file is passed via stdin, e.g., - // `nl -ba README.md | sed -n '1,200p'` - if ( - cmd1 === "-n" && - isValidSedNArg(cmd2) && - (command.length === 3 || - (typeof cmd3 === "string" && command.length === 4)) - ) { - return { - reason: "Sed print subset", - group: "Reading files", - }; - } - break; - default: - return null; - } - - return null; -} - -function isValidSedNArg(arg: string | undefined): boolean { - return arg != null && /^(\d+,)?\d+p$/.test(arg); -} - -const UNSAFE_OPTIONS_FOR_FIND_COMMAND: ReadonlySet = new Set([ - // Options that can execute arbitrary commands. - "-exec", - "-execdir", - "-ok", - "-okdir", - // Option that deletes matching files. - "-delete", - // Options that write pathnames to a file. - "-fls", - "-fprint", - "-fprint0", - "-fprintf", -]); - -// Ripgrep options that are considered unsafe because they may execute -// arbitrary commands or spawn auxiliary processes. -const UNSAFE_OPTIONS_FOR_RIPGREP_WITH_ARGS: ReadonlySet = new Set([ - // Executes an arbitrary command for each matching file. - "--pre", - // Allows custom hostname command which could leak environment details. - "--hostname-bin", -]); - -const UNSAFE_OPTIONS_FOR_RIPGREP_WITHOUT_ARGS: ReadonlySet = new Set([ - // Enables searching inside archives which triggers external decompression - // utilities – reject out of an abundance of caution. - "--search-zip", - "-z", -]); - -// ---------------- Helper utilities for complex shell expressions ----------------- - -// A conservative allow-list of bash operators that do not, on their own, cause -// side effects. Redirections (>, >>, <, etc.) and command substitution `$()` -// are intentionally excluded. Parentheses used for grouping are treated as -// strings by `shell-quote`, so we do not add them here. Reference: -// https://github.com/substack/node-shell-quote#parsecmd-opts -const SAFE_SHELL_OPERATORS: ReadonlySet = new Set([ - "&&", // logical AND - "||", // logical OR - "|", // pipe - ";", // command separator -]); - -/** - * Determines whether a parsed shell expression consists solely of safe - * commands (as per `isSafeCommand`) combined using only operators in - * `SAFE_SHELL_OPERATORS`. - * - * If entirely safe, returns the reason/group from the *first* command - * segment so callers can surface a meaningful description. Otherwise returns - * null. - */ -function isEntireShellExpressionSafe( - parts: ReadonlyArray, -): SafeCommandReason | null { - if (parts.length === 0) { - return null; - } - - try { - // Collect command segments delimited by operators. `shell-quote` represents - // subshell grouping parentheses as literal strings "(" and ")"; treat them - // as unsafe to keep the logic simple (since subshells could introduce - // unexpected scope changes). - - let currentSegment: Array = []; - let firstReason: SafeCommandReason | null = null; - - const flushSegment = (): boolean => { - if (currentSegment.length === 0) { - return true; // nothing to validate (possible leading operator) - } - const assessment = isSafeCommand(currentSegment); - if (assessment == null) { - return false; - } - if (firstReason == null) { - firstReason = assessment; - } - currentSegment = []; - return true; - }; - - for (const part of parts) { - if (typeof part === "string") { - // If this string looks like an open/close parenthesis or brace, treat as - // unsafe to avoid parsing complexity. - if (part === "(" || part === ")" || part === "{" || part === "}") { - return null; - } - currentSegment.push(part); - } else if (isParseEntryWithOp(part)) { - // Validate the segment accumulated so far. - if (!flushSegment()) { - return null; - } - - // Validate the operator itself. - if (!SAFE_SHELL_OPERATORS.has(part.op)) { - return null; - } - } else { - // Unknown token type - return null; - } - } - - // Validate any trailing command segment. - if (!flushSegment()) { - return null; - } - - return firstReason; - } catch (_err) { - // If there's any kind of failure, just bail out and return null. - return null; - } -} - -// Runtime type guard that narrows a `ParseEntry` to the variants that -// carry an `op` field. Using a dedicated function avoids the need for -// inline type assertions and makes the narrowing reusable and explicit. -function isParseEntryWithOp( - entry: ParseEntry, -): entry is { op: ControlOperator } | { op: "glob"; pattern: string } { - return ( - typeof entry === "object" && - entry != null && - // Using the safe `in` operator keeps the check property-safe even when - // `entry` is a `string`. - "op" in entry && - typeof (entry as { op?: unknown }).op === "string" - ); -} diff --git a/codex-cli/src/cli-singlepass.tsx b/codex-cli/src/cli-singlepass.tsx deleted file mode 100644 index 49f25c38e5..0000000000 --- a/codex-cli/src/cli-singlepass.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { AppConfig } from "./utils/config"; - -import { SinglePassApp } from "./components/singlepass-cli-app"; -import { render } from "ink"; -import React from "react"; - -export async function runSinglePass({ - originalPrompt, - config, - rootPath, -}: { - originalPrompt?: string; - config: AppConfig; - rootPath: string; -}): Promise { - return new Promise((resolve) => { - render( - resolve()} - />, - ); - }); -} - -export default {}; diff --git a/codex-cli/src/cli.tsx b/codex-cli/src/cli.tsx deleted file mode 100644 index 0442a6c377..0000000000 --- a/codex-cli/src/cli.tsx +++ /dev/null @@ -1,740 +0,0 @@ -#!/usr/bin/env node -import "dotenv/config"; - -// Exit early if on an older version of Node.js (< 22) -const major = process.versions.node.split(".").map(Number)[0]!; -if (major < 22) { - // eslint-disable-next-line no-console - console.error( - "\n" + - "Codex CLI requires Node.js version 22 or newer.\n" + - `You are running Node.js v${process.versions.node}.\n` + - "Please upgrade Node.js: https://nodejs.org/en/download/\n", - ); - process.exit(1); -} - -// Hack to suppress deprecation warnings (punycode) -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(process as any).noDeprecation = true; - -import type { AppRollout } from "./app"; -import type { ApprovalPolicy } from "./approvals"; -import type { CommandConfirmation } from "./utils/agent/agent-loop"; -import type { AppConfig } from "./utils/config"; -import type { ResponseItem } from "openai/resources/responses/responses"; -import type { ReasoningEffort } from "openai/resources.mjs"; - -import App from "./app"; -import { runSinglePass } from "./cli-singlepass"; -import SessionsOverlay from "./components/sessions-overlay.js"; -import { AgentLoop } from "./utils/agent/agent-loop"; -import { ReviewDecision } from "./utils/agent/review"; -import { AutoApprovalMode } from "./utils/auto-approval-mode"; -import { checkForUpdates } from "./utils/check-updates"; -import { - loadConfig, - PRETTY_PRINT, - INSTRUCTIONS_FILEPATH, -} from "./utils/config"; -import { - getApiKey as fetchApiKey, - maybeRedeemCredits, -} from "./utils/get-api-key"; -import { createInputItem } from "./utils/input-utils"; -import { initLogger } from "./utils/logger/log"; -import { isModelSupportedForResponses } from "./utils/model-utils.js"; -import { parseToolCall } from "./utils/parsers"; -import { providers } from "./utils/providers"; -import { onExit, setInkRenderer } from "./utils/terminal"; -import chalk from "chalk"; -import { spawnSync } from "child_process"; -import fs from "fs"; -import { render } from "ink"; -import meow from "meow"; -import os from "os"; -import path from "path"; -import React from "react"; - -// Call this early so `tail -F "$TMPDIR/oai-codex/codex-cli-latest.log"` works -// immediately. This must be run with DEBUG=1 for logging to work. -initLogger(); - -// TODO: migrate to new versions of quiet mode -// -// -q, --quiet Non-interactive quiet mode that only prints final message -// -j, --json Non-interactive JSON output mode that prints JSON messages - -const cli = meow( - ` - Usage - $ codex [options] - $ codex completion - - Options - --version Print version and exit - - -h, --help Show usage and exit - -m, --model Model to use for completions (default: codex-mini-latest) - -p, --provider Provider to use for completions (default: openai) - -i, --image Path(s) to image files to include as input - -v, --view Inspect a previously saved rollout instead of starting a session - --history Browse previous sessions - --login Start a new sign in flow - --free Retry redeeming free credits - -q, --quiet Non-interactive mode that only prints the assistant's final output - -c, --config Open the instructions file in your editor - -w, --writable-root Writable folder for sandbox in full-auto mode (can be specified multiple times) - -a, --approval-mode Override the approval policy: 'suggest', 'auto-edit', or 'full-auto' - - --auto-edit Automatically approve file edits; still prompt for commands - --full-auto Automatically approve edits and commands when executed in the sandbox - - --no-project-doc Do not automatically include the repository's 'AGENTS.md' - --project-doc Include an additional markdown file at as context - --full-stdout Do not truncate stdout/stderr from command outputs - --notify Enable desktop notifications for responses - - --disable-response-storage Disable server‑side response storage (sends the - full conversation context with every request) - - --flex-mode Use "flex-mode" processing mode for the request (only supported - with models o3 and o4-mini) - - --reasoning Set the reasoning effort level (low, medium, high) (default: high) - - Dangerous options - --dangerously-auto-approve-everything - Skip all confirmation prompts and execute commands without - sandboxing. Intended solely for ephemeral local testing. - - Experimental options - -f, --full-context Launch in "full-context" mode which loads the entire repository - into context and applies a batch of edits in one go. Incompatible - with all other flags, except for --model. - - Examples - $ codex "Write and run a python program that prints ASCII art" - $ codex -q "fix build issues" - $ codex completion bash -`, - { - importMeta: import.meta, - autoHelp: true, - flags: { - // misc - help: { type: "boolean", aliases: ["h"] }, - version: { type: "boolean", description: "Print version and exit" }, - view: { type: "string" }, - history: { type: "boolean", description: "Browse previous sessions" }, - login: { type: "boolean", description: "Force a new sign in flow" }, - free: { type: "boolean", description: "Retry redeeming free credits" }, - model: { type: "string", aliases: ["m"] }, - provider: { type: "string", aliases: ["p"] }, - image: { type: "string", isMultiple: true, aliases: ["i"] }, - quiet: { - type: "boolean", - aliases: ["q"], - description: "Non-interactive quiet mode", - }, - config: { - type: "boolean", - aliases: ["c"], - description: "Open the instructions file in your editor", - }, - dangerouslyAutoApproveEverything: { - type: "boolean", - description: - "Automatically approve all commands without prompting. This is EXTREMELY DANGEROUS and should only be used in trusted environments.", - }, - autoEdit: { - type: "boolean", - description: "Automatically approve edits; prompt for commands.", - }, - fullAuto: { - type: "boolean", - description: - "Automatically run commands in a sandbox; only prompt for failures.", - }, - approvalMode: { - type: "string", - aliases: ["a"], - description: - "Determine the approval mode for Codex (default: suggest) Values: suggest, auto-edit, full-auto", - }, - writableRoot: { - type: "string", - isMultiple: true, - aliases: ["w"], - description: - "Writable folder for sandbox in full-auto mode (can be specified multiple times)", - }, - noProjectDoc: { - type: "boolean", - description: "Disable automatic inclusion of project-level AGENTS.md", - }, - projectDoc: { - type: "string", - description: "Path to a markdown file to include as project doc", - }, - flexMode: { - type: "boolean", - description: - "Enable the flex-mode service tier (only supported by models o3 and o4-mini)", - }, - fullStdout: { - type: "boolean", - description: - "Disable truncation of command stdout/stderr messages (show everything)", - aliases: ["no-truncate"], - }, - reasoning: { - type: "string", - description: "Set the reasoning effort level (low, medium, high)", - choices: ["low", "medium", "high"], - default: "high", - }, - // Notification - notify: { - type: "boolean", - description: "Enable desktop notifications for responses", - }, - - disableResponseStorage: { - type: "boolean", - description: - "Disable server-side response storage (sends full conversation context with every request)", - }, - - // Experimental mode where whole directory is loaded in context and model is requested - // to make code edits in a single pass. - fullContext: { - type: "boolean", - aliases: ["f"], - description: `Run in full-context editing approach. The model is given the whole code - directory as context and performs changes in one go without acting.`, - }, - }, - }, -); - -// --------------------------------------------------------------------------- -// Global flag handling -// --------------------------------------------------------------------------- - -// Handle 'completion' subcommand before any prompting or API calls -if (cli.input[0] === "completion") { - const shell = cli.input[1] || "bash"; - const scripts: Record = { - bash: `# bash completion for codex -_codex_completion() { - local cur - cur="\${COMP_WORDS[COMP_CWORD]}" - COMPREPLY=( $(compgen -o default -o filenames -- "\${cur}") ) -} -complete -F _codex_completion codex`, - zsh: `# zsh completion for codex -#compdef codex - -_codex() { - _arguments '*:filename:_files' -} -_codex`, - fish: `# fish completion for codex -complete -c codex -a '(__fish_complete_path)' -d 'file path'`, - }; - const script = scripts[shell]; - if (!script) { - // eslint-disable-next-line no-console - console.error(`Unsupported shell: ${shell}`); - process.exit(1); - } - // eslint-disable-next-line no-console - console.log(script); - process.exit(0); -} - -// For --help, show help and exit. -if (cli.flags.help) { - cli.showHelp(); -} - -// For --config, open custom instructions file in editor and exit. -if (cli.flags.config) { - try { - loadConfig(); // Ensures the file is created if it doesn't already exit. - } catch { - // ignore errors - } - - const filePath = INSTRUCTIONS_FILEPATH; - const editor = - process.env["EDITOR"] || (process.platform === "win32" ? "notepad" : "vi"); - spawnSync(editor, [filePath], { stdio: "inherit" }); - process.exit(0); -} - -// --------------------------------------------------------------------------- -// API key handling -// --------------------------------------------------------------------------- - -const fullContextMode = Boolean(cli.flags.fullContext); -let config = loadConfig(undefined, undefined, { - cwd: process.cwd(), - disableProjectDoc: Boolean(cli.flags.noProjectDoc), - projectDocPath: cli.flags.projectDoc, - isFullContext: fullContextMode, -}); - -// `prompt` can be updated later when the user resumes a previous session -// via the `--history` flag. Therefore it must be declared with `let` rather -// than `const`. -let prompt = cli.input[0]; -const model = cli.flags.model ?? config.model; -const imagePaths = cli.flags.image; -const provider = cli.flags.provider ?? config.provider ?? "openai"; - -const client = { - issuer: "https://auth.openai.com", - client_id: "app_EMoamEEZ73f0CkXaXp7hrann", -}; - -let apiKey = ""; -let savedTokens: - | { - id_token?: string; - access_token?: string; - refresh_token: string; - } - | undefined; - -// Try to load existing auth file if present -try { - const home = os.homedir(); - const authDir = path.join(home, ".codex"); - const authFile = path.join(authDir, "auth.json"); - if (fs.existsSync(authFile)) { - const data = JSON.parse(fs.readFileSync(authFile, "utf-8")); - savedTokens = data.tokens; - const lastRefreshTime = data.last_refresh - ? new Date(data.last_refresh).getTime() - : 0; - const expired = Date.now() - lastRefreshTime > 28 * 24 * 60 * 60 * 1000; - if (data.OPENAI_API_KEY && !expired) { - apiKey = data.OPENAI_API_KEY; - } - } -} catch { - // ignore errors -} - -// Get provider-specific API key if not OpenAI -if (provider.toLowerCase() !== "openai") { - const providerInfo = providers[provider.toLowerCase()]; - if (providerInfo) { - const providerApiKey = process.env[providerInfo.envKey]; - if (providerApiKey) { - apiKey = providerApiKey; - } - } -} - -// Only proceed with OpenAI auth flow if: -// 1. Provider is OpenAI and no API key is set, or -// 2. Login flag is explicitly set -if (provider.toLowerCase() === "openai" && !apiKey) { - if (cli.flags.login) { - apiKey = await fetchApiKey(client.issuer, client.client_id); - try { - const home = os.homedir(); - const authDir = path.join(home, ".codex"); - const authFile = path.join(authDir, "auth.json"); - if (fs.existsSync(authFile)) { - const data = JSON.parse(fs.readFileSync(authFile, "utf-8")); - savedTokens = data.tokens; - } - } catch { - /* ignore */ - } - } else { - apiKey = await fetchApiKey(client.issuer, client.client_id); - } -} - -// Ensure the API key is available as an environment variable for legacy code -process.env["OPENAI_API_KEY"] = apiKey; - -// Only attempt credit redemption for OpenAI provider -if (cli.flags.free && provider.toLowerCase() === "openai") { - // eslint-disable-next-line no-console - console.log(`${chalk.bold("codex --free")} attempting to redeem credits...`); - if (!savedTokens?.refresh_token) { - apiKey = await fetchApiKey(client.issuer, client.client_id, true); - // fetchApiKey includes credit redemption as the end of the flow - } else { - await maybeRedeemCredits( - client.issuer, - client.client_id, - savedTokens.refresh_token, - savedTokens.id_token, - ); - } -} - -// Set of providers that don't require API keys -const NO_API_KEY_REQUIRED = new Set(["ollama"]); - -// Skip API key validation for providers that don't require an API key -if (!apiKey && !NO_API_KEY_REQUIRED.has(provider.toLowerCase())) { - // eslint-disable-next-line no-console - console.error( - `\n${chalk.red(`Missing ${provider} API key.`)}\n\n` + - `Set the environment variable ${chalk.bold( - `${provider.toUpperCase()}_API_KEY`, - )} ` + - `and re-run this command.\n` + - `${ - provider.toLowerCase() === "openai" - ? `You can create a key here: ${chalk.bold( - chalk.underline("https://platform.openai.com/account/api-keys"), - )}\n` - : provider.toLowerCase() === "azure" - ? `You can create a ${chalk.bold( - `${provider.toUpperCase()}_OPENAI_API_KEY`, - )} ` + - `in Azure AI Foundry portal at ${chalk.bold(chalk.underline("https://ai.azure.com"))}.\n` - : provider.toLowerCase() === "gemini" - ? `You can create a ${chalk.bold( - `${provider.toUpperCase()}_API_KEY`, - )} ` + `in the ${chalk.bold(`Google AI Studio`)}.\n` - : `You can create a ${chalk.bold( - `${provider.toUpperCase()}_API_KEY`, - )} ` + `in the ${chalk.bold(`${provider}`)} dashboard.\n` - }`, - ); - process.exit(1); -} - -const flagPresent = Object.hasOwn(cli.flags, "disableResponseStorage"); - -const disableResponseStorage = flagPresent - ? Boolean(cli.flags.disableResponseStorage) // value user actually passed - : (config.disableResponseStorage ?? false); // fall back to YAML, default to false - -config = { - apiKey, - ...config, - model: model ?? config.model, - notify: Boolean(cli.flags.notify), - reasoningEffort: - (cli.flags.reasoning as ReasoningEffort | undefined) ?? "medium", - flexMode: cli.flags.flexMode || (config.flexMode ?? false), - provider, - disableResponseStorage, -}; - -// Check for updates after loading config. This is important because we write state file in -// the config dir. -try { - await checkForUpdates(); -} catch { - // ignore -} - -// For --flex-mode, validate and exit if incorrect. -if (config.flexMode) { - const allowedFlexModels = new Set(["o3", "o4-mini"]); - if (!allowedFlexModels.has(config.model)) { - if (cli.flags.flexMode) { - // eslint-disable-next-line no-console - console.error( - `The --flex-mode option is only supported when using the 'o3' or 'o4-mini' models. ` + - `Current model: '${config.model}'.`, - ); - process.exit(1); - } else { - config.flexMode = false; - } - } -} - -if ( - !(await isModelSupportedForResponses(provider, config.model)) && - (!provider || provider.toLowerCase() === "openai") -) { - // eslint-disable-next-line no-console - console.error( - `The model "${config.model}" does not appear in the list of models ` + - `available to your account. Double-check the spelling (use\n` + - ` openai models list\n` + - `to see the full list) or choose another model with the --model flag.`, - ); - process.exit(1); -} - -let rollout: AppRollout | undefined; - -// For --history, show session selector and optionally update prompt or rollout. -if (cli.flags.history) { - const result: { path: string; mode: "view" | "resume" } | null = - await new Promise((resolve) => { - const instance = render( - React.createElement(SessionsOverlay, { - onView: (p: string) => { - instance.unmount(); - resolve({ path: p, mode: "view" }); - }, - onResume: (p: string) => { - instance.unmount(); - resolve({ path: p, mode: "resume" }); - }, - onExit: () => { - instance.unmount(); - resolve(null); - }, - }), - ); - }); - - if (!result) { - process.exit(0); - } - - if (result.mode === "view") { - try { - const content = fs.readFileSync(result.path, "utf-8"); - rollout = JSON.parse(content) as AppRollout; - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error reading session file:", error); - process.exit(1); - } - } else { - prompt = `Resume this session: ${result.path}`; - } -} - -// For --view, optionally load an existing rollout from disk, display it and exit. -if (cli.flags.view) { - const viewPath = cli.flags.view; - const absolutePath = path.isAbsolute(viewPath) - ? viewPath - : path.join(process.cwd(), viewPath); - try { - const content = fs.readFileSync(absolutePath, "utf-8"); - rollout = JSON.parse(content) as AppRollout; - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error reading rollout file:", error); - process.exit(1); - } -} - -// For --fullcontext, run the separate cli entrypoint and exit. -if (fullContextMode) { - await runSinglePass({ - originalPrompt: prompt, - config, - rootPath: process.cwd(), - }); - onExit(); - process.exit(0); -} - -// Ensure that all values in additionalWritableRoots are absolute paths. -const additionalWritableRoots: ReadonlyArray = ( - cli.flags.writableRoot ?? [] -).map((p) => path.resolve(p)); - -// For --quiet, run the cli without user interactions and exit. -if (cli.flags.quiet) { - process.env["CODEX_QUIET_MODE"] = "1"; - if (!prompt || prompt.trim() === "") { - // eslint-disable-next-line no-console - console.error( - 'Quiet mode requires a prompt string, e.g.,: codex -q "Fix bug #123 in the foobar project"', - ); - process.exit(1); - } - - // Determine approval policy for quiet mode based on flags - const quietApprovalPolicy: ApprovalPolicy = - cli.flags.fullAuto || cli.flags.approvalMode === "full-auto" - ? AutoApprovalMode.FULL_AUTO - : cli.flags.autoEdit || cli.flags.approvalMode === "auto-edit" - ? AutoApprovalMode.AUTO_EDIT - : config.approvalMode || AutoApprovalMode.SUGGEST; - - await runQuietMode({ - prompt, - imagePaths: imagePaths || [], - approvalPolicy: quietApprovalPolicy, - additionalWritableRoots, - config, - }); - onExit(); - process.exit(0); -} - -// Default to the "suggest" policy. -// Determine the approval policy to use in interactive mode. -// -// Priority (highest → lowest): -// 1. --fullAuto – run everything automatically in a sandbox. -// 2. --dangerouslyAutoApproveEverything – run everything **without** a sandbox -// or prompts. This is intended for completely trusted environments. Since -// it is more dangerous than --fullAuto we deliberately give it lower -// priority so a user specifying both flags still gets the safer behaviour. -// 3. --autoEdit – automatically approve edits, but prompt for commands. -// 4. config.approvalMode - use the approvalMode setting from ~/.codex/config.json. -// 5. Default – suggest mode (prompt for everything). - -const approvalPolicy: ApprovalPolicy = - cli.flags.fullAuto || cli.flags.approvalMode === "full-auto" - ? AutoApprovalMode.FULL_AUTO - : cli.flags.autoEdit || cli.flags.approvalMode === "auto-edit" - ? AutoApprovalMode.AUTO_EDIT - : config.approvalMode || AutoApprovalMode.SUGGEST; - -const instance = render( - , - { - patchConsole: process.env["DEBUG"] ? false : true, - }, -); -setInkRenderer(instance); - -function formatResponseItemForQuietMode(item: ResponseItem): string { - if (!PRETTY_PRINT) { - return JSON.stringify(item); - } - switch (item.type) { - case "message": { - const role = item.role === "assistant" ? "assistant" : item.role; - const txt = item.content - .map((c) => { - if (c.type === "output_text" || c.type === "input_text") { - return c.text; - } - if (c.type === "input_image") { - return ""; - } - if (c.type === "input_file") { - return c.filename; - } - if (c.type === "refusal") { - return c.refusal; - } - return "?"; - }) - .join(" "); - return `${role}: ${txt}`; - } - case "function_call": { - const details = parseToolCall(item); - return `$ ${details?.cmdReadableText ?? item.name}`; - } - case "function_call_output": { - // @ts-expect-error metadata unknown on ResponseFunctionToolCallOutputItem - const meta = item.metadata as ExecOutputMetadata; - const parts: Array = []; - if (typeof meta?.exit_code === "number") { - parts.push(`code: ${meta.exit_code}`); - } - if (typeof meta?.duration_seconds === "number") { - parts.push(`duration: ${meta.duration_seconds}s`); - } - const header = parts.length > 0 ? ` (${parts.join(", ")})` : ""; - return `command.stdout${header}\n${item.output}`; - } - default: { - return JSON.stringify(item); - } - } -} - -async function runQuietMode({ - prompt, - imagePaths, - approvalPolicy, - additionalWritableRoots, - config, -}: { - prompt: string; - imagePaths: Array; - approvalPolicy: ApprovalPolicy; - additionalWritableRoots: ReadonlyArray; - config: AppConfig; -}): Promise { - const agent = new AgentLoop({ - model: config.model, - config: config, - instructions: config.instructions, - provider: config.provider, - approvalPolicy, - additionalWritableRoots, - disableResponseStorage: config.disableResponseStorage, - onItem: (item: ResponseItem) => { - // eslint-disable-next-line no-console - console.log(formatResponseItemForQuietMode(item)); - }, - onLoading: () => { - /* intentionally ignored in quiet mode */ - }, - getCommandConfirmation: ( - _command: Array, - ): Promise => { - // In quiet mode, default to NO_CONTINUE, except when in full-auto mode - const reviewDecision = - approvalPolicy === AutoApprovalMode.FULL_AUTO - ? ReviewDecision.YES - : ReviewDecision.NO_CONTINUE; - return Promise.resolve({ review: reviewDecision }); - }, - onLastResponseId: () => { - /* intentionally ignored in quiet mode */ - }, - }); - - const inputItem = await createInputItem(prompt, imagePaths); - await agent.run([inputItem]); -} - -const exit = () => { - onExit(); - process.exit(0); -}; - -process.on("SIGINT", exit); -process.on("SIGQUIT", exit); -process.on("SIGTERM", exit); - -// --------------------------------------------------------------------------- -// Fallback for Ctrl-C when stdin is in raw-mode -// --------------------------------------------------------------------------- - -if (process.stdin.isTTY) { - // Ensure we do not leave the terminal in raw mode if the user presses - // Ctrl-C while some other component has focus and Ink is intercepting - // input. Node does *not* emit a SIGINT in raw-mode, so we listen for the - // corresponding byte (0x03) ourselves and trigger a graceful shutdown. - const onRawData = (data: Buffer | string): void => { - const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; - if (str === "\u0003") { - exit(); - } - }; - process.stdin.on("data", onRawData); -} - -// Ensure terminal clean-up always runs, even when other code calls -// `process.exit()` directly. -process.once("exit", onExit); diff --git a/codex-cli/src/components/approval-mode-overlay.tsx b/codex-cli/src/components/approval-mode-overlay.tsx deleted file mode 100644 index dd079e3b2c..0000000000 --- a/codex-cli/src/components/approval-mode-overlay.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import TypeaheadOverlay from "./typeahead-overlay.js"; -import { AutoApprovalMode } from "../utils/auto-approval-mode.js"; -import { Text } from "ink"; -import React from "react"; - -type Props = { - currentMode: string; - onSelect: (mode: string) => void; - onExit: () => void; -}; - -/** - * Overlay to switch between the different automatic‑approval policies. - * - * The list of available modes is derived from the AutoApprovalMode enum so we - * stay in sync with the core agent behaviour. It re‑uses the generic - * TypeaheadOverlay component for the actual UI/UX. - */ -export default function ApprovalModeOverlay({ - currentMode, - onSelect, - onExit, -}: Props): JSX.Element { - const items = React.useMemo( - () => - Object.values(AutoApprovalMode).map((m) => ({ - label: m, - value: m, - })), - [], - ); - - return ( - - Current mode: {currentMode} - - } - initialItems={items} - currentValue={currentMode} - onSelect={onSelect} - onExit={onExit} - /> - ); -} diff --git a/codex-cli/src/components/chat/message-history.tsx b/codex-cli/src/components/chat/message-history.tsx deleted file mode 100644 index bab6b1663f..0000000000 --- a/codex-cli/src/components/chat/message-history.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import type { TerminalHeaderProps } from "./terminal-header.js"; -import type { GroupedResponseItem } from "./use-message-grouping.js"; -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; -import type { FileOpenerScheme } from "src/utils/config.js"; - -import TerminalChatResponseItem from "./terminal-chat-response-item.js"; -import TerminalHeader from "./terminal-header.js"; -import { Box, Static } from "ink"; -import React from "react"; - -// A batch entry can either be a standalone response item or a grouped set of -// items (e.g. auto‑approved tool‑call batches) that should be rendered -// together. -type BatchEntry = { item?: ResponseItem; group?: GroupedResponseItem }; -type MessageHistoryProps = { - batch: Array; - groupCounts: Record; - items: Array; - userMsgCount: number; - confirmationPrompt: React.ReactNode; - loading: boolean; - headerProps: TerminalHeaderProps; - fileOpener: FileOpenerScheme | undefined; -}; - -const MessageHistory: React.FC = ({ - batch, - headerProps, - fileOpener, -}) => { - const messages = batch.map(({ item }) => item!); - - return ( - - {/* - * The Static component receives a mixed array of the literal string - * "header" plus the streamed ResponseItem objects. After filtering out - * the header entry we can safely treat the remaining values as - * ResponseItem, however TypeScript cannot infer the refined type from - * the runtime check and therefore reports property‑access errors. - * - * A short cast after the refinement keeps the implementation tidy while - * preserving type‑safety. - */} - - {(item, index) => { - if (item === "header") { - return ; - } - - // After the guard above `item` can only be a ResponseItem. - const message = item as ResponseItem; - return ( - - - - ); - }} - - - ); -}; - -export default React.memo(MessageHistory); diff --git a/codex-cli/src/components/chat/multiline-editor.tsx b/codex-cli/src/components/chat/multiline-editor.tsx deleted file mode 100644 index 6b24bc27f2..0000000000 --- a/codex-cli/src/components/chat/multiline-editor.tsx +++ /dev/null @@ -1,392 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { useTerminalSize } from "../../hooks/use-terminal-size"; -import TextBuffer from "../../text-buffer.js"; -import chalk from "chalk"; -import { Box, Text, useInput } from "ink"; -import { EventEmitter } from "node:events"; -import React, { useRef, useState } from "react"; - -/* -------------------------------------------------------------------------- - * Polyfill missing `ref()` / `unref()` methods on the mock `Stdin` stream - * provided by `ink-testing-library`. - * - * The real `process.stdin` object exposed by Node.js inherits these methods - * from `Socket`, but the lightweight stub used in tests only extends - * `EventEmitter`. Ink calls the two methods when enabling/disabling raw - * mode, so make them harmless no-ops when they're absent to avoid runtime - * failures during unit tests. - * ----------------------------------------------------------------------- */ - -// Cast through `unknown` ➜ `any` to avoid the `TS2352`/`TS4111` complaints -// when augmenting the prototype with the stubbed `ref`/`unref` methods in the -// test environment. Using `any` here is acceptable because we purposefully -// monkey‑patch internals of Node's `EventEmitter` solely for the benefit of -// Ink's stdin stub – type‑safety is not a primary concern at this boundary. -// -const proto: any = EventEmitter.prototype; - -if (typeof proto["ref"] !== "function") { - proto["ref"] = function ref() {}; -} -if (typeof proto["unref"] !== "function") { - proto["unref"] = function unref() {}; -} - -/* - * The `ink-testing-library` stub emits only a `data` event when its `stdin` - * mock receives `write()` calls. Ink, however, listens for `readable` and - * uses the `read()` method to fetch the buffered chunk. Bridge the gap by - * hooking into `EventEmitter.emit` so that every `data` emission also: - * 1. Buffers the chunk for a subsequent `read()` call, and - * 2. Triggers a `readable` event, matching the contract expected by Ink. - */ - -// Preserve original emit to avoid infinite recursion. -// eslint‑disable‑next‑line @typescript-eslint/no‑unsafe‑assignment -const originalEmit = proto["emit"] as (...args: Array) => boolean; - -proto["emit"] = function patchedEmit( - this: any, - event: string, - ...args: Array -): boolean { - if (event === "data") { - const chunk = args[0] as string; - - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log("[MultilineTextEditor:stdin] data", JSON.stringify(chunk)); - } - // Store carriage returns as‑is so that Ink can distinguish between plain - // ("\r") and a bare line‑feed ("\n"). This matters because Ink's - // `parseKeypress` treats "\r" as key.name === "return", whereas "\n" maps - // to "enter" – allowing us to differentiate between plain Enter (submit) - // and Shift+Enter (insert newline) inside `useInput`. - - // Identify the lightweight testing stub: lacks `.read()` but exposes - // `.setRawMode()` and `isTTY` similar to the real TTY stream. - if ( - !(this as any)._inkIsStub && - typeof (this as any).setRawMode === "function" && - typeof (this as any).isTTY === "boolean" && - typeof (this as any).read !== "function" - ) { - (this as any)._inkIsStub = true; - - // Provide a minimal `read()` shim so Ink can pull queued chunks. - (this as any).read = function read() { - const ret = (this as any)._inkBuffered ?? null; - (this as any)._inkBuffered = null; - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log("[MultilineTextEditor:stdin.read]", JSON.stringify(ret)); - } - return ret; - }; - } - - if ((this as any)._inkIsStub) { - // Buffer the payload so that `read()` can synchronously retrieve it. - if (typeof (this as any)._inkBuffered === "string") { - (this as any)._inkBuffered += chunk; - } else { - (this as any)._inkBuffered = chunk; - } - - // Notify listeners that data is ready in a way Ink understands. - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log( - "[MultilineTextEditor:stdin] -> readable", - JSON.stringify(chunk), - ); - } - originalEmit.call(this, "readable"); - } - } - - // Forward the original event. - return originalEmit.call(this, event, ...args); -}; - -export interface MultilineTextEditorProps { - // Initial contents. - readonly initialText?: string; - - // Visible width. - readonly width?: number; - - // Visible height. - readonly height?: number; - - // Called when the user submits (plain key). - readonly onSubmit?: (text: string) => void; - - // Capture keyboard input. - readonly focus?: boolean; - - // Called when the internal text buffer updates. - readonly onChange?: (text: string) => void; - - // Optional initial cursor position (character offset) - readonly initialCursorOffset?: number; -} - -// Expose a minimal imperative API so parent components (e.g. TerminalChatInput) -// can query the caret position to implement behaviours like history -// navigation that depend on whether the cursor sits on the first/last line. -export interface MultilineTextEditorHandle { - /** Current caret row */ - getRow(): number; - /** Current caret column */ - getCol(): number; - /** Total number of lines in the buffer */ - getLineCount(): number; - /** Helper: caret is on the very first row */ - isCursorAtFirstRow(): boolean; - /** Helper: caret is on the very last row */ - isCursorAtLastRow(): boolean; - /** Full text contents */ - getText(): string; - /** Move the cursor to the end of the text */ - moveCursorToEnd(): void; -} - -const MultilineTextEditorInner = ( - { - initialText = "", - // Width can be provided by the caller. When omitted we fall back to the - // current terminal size (minus some padding handled by `useTerminalSize`). - width, - height = 10, - onSubmit, - focus = true, - onChange, - initialCursorOffset, - }: MultilineTextEditorProps, - ref: React.Ref, -): React.ReactElement => { - // --------------------------------------------------------------------------- - // Editor State - // --------------------------------------------------------------------------- - - const buffer = useRef(new TextBuffer(initialText, initialCursorOffset)); - const [version, setVersion] = useState(0); - - // Keep track of the current terminal size so that the editor grows/shrinks - // with the window. `useTerminalSize` already subtracts a small horizontal - // padding so that we don't butt up right against the edge. - const terminalSize = useTerminalSize(); - - // If the caller didn't specify a width we dynamically choose one based on - // the terminal's current column count. We still enforce a reasonable - // minimum so that the UI never becomes unusably small. - const effectiveWidth = Math.max(20, width ?? terminalSize.columns); - - // --------------------------------------------------------------------------- - // Keyboard handling. - // --------------------------------------------------------------------------- - - useInput( - (input, key) => { - if (!focus) { - return; - } - - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log("[MultilineTextEditor] event", { input, key }); - } - - // 1a) CSI-u / modifyOtherKeys *mode 2* (Ink strips initial ESC, so we - // start with '[') – format: "[;u". - if (input.startsWith("[") && input.endsWith("u")) { - const m = input.match(/^\[([0-9]+);([0-9]+)u$/); - if (m && m[1] === "13") { - const mod = Number(m[2]); - // In xterm's encoding: bit-1 (value 2) is Shift. Everything >1 that - // isn't exactly 1 means some modifier was held. We treat *shift or - // alt present* (2,3,4,6,8,9) as newline; Ctrl (bit-2 / value 4) - // triggers submit. See xterm/DEC modifyOtherKeys docs. - - const hasCtrl = Math.floor(mod / 4) % 2 === 1; - if (hasCtrl) { - if (onSubmit) { - onSubmit(buffer.current.getText()); - } - } else { - buffer.current.newline(); - } - setVersion((v) => v + 1); - return; - } - } - - // 1b) CSI-~ / modifyOtherKeys *mode 1* – format: "[27;;~". - // Terminals such as iTerm2 (default), older xterm versions, or when - // modifyOtherKeys=1 is configured, emit this legacy sequence. We - // translate it to the same behaviour as the mode‑2 variant above so - // that Shift+Enter (newline) / Ctrl+Enter (submit) work regardless - // of the user’s terminal settings. - if (input.startsWith("[27;") && input.endsWith("~")) { - const m = input.match(/^\[27;([0-9]+);13~$/); - if (m) { - const mod = Number(m[1]); - const hasCtrl = Math.floor(mod / 4) % 2 === 1; - - if (hasCtrl) { - if (onSubmit) { - onSubmit(buffer.current.getText()); - } - } else { - buffer.current.newline(); - } - setVersion((v) => v + 1); - return; - } - } - - // 2) Single‑byte control chars ------------------------------------------------ - if (input === "\n") { - // Ctrl+J or pasted newline → insert newline. - buffer.current.newline(); - setVersion((v) => v + 1); - return; - } - - if (input === "\r") { - // Plain Enter – submit (works on all basic terminals). - if (onSubmit) { - onSubmit(buffer.current.getText()); - } - return; - } - - // Let fall through so the parent handler (if any) can act on it. - - // Delegate remaining keys to our pure TextBuffer - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log("[MultilineTextEditor] key event", { input, key }); - } - - const modified = buffer.current.handleInput( - input, - key as Record, - { height, width: effectiveWidth }, - ); - if (modified) { - setVersion((v) => v + 1); - } - - const newText = buffer.current.getText(); - if (onChange) { - onChange(newText); - } - }, - { isActive: focus }, - ); - - // --------------------------------------------------------------------------- - // Rendering helpers. - // --------------------------------------------------------------------------- - - /* ------------------------------------------------------------------------- */ - /* Imperative handle – expose a read‑only view of caret & buffer geometry */ - /* ------------------------------------------------------------------------- */ - - React.useImperativeHandle( - ref, - () => ({ - getRow: () => buffer.current.getCursor()[0], - getCol: () => buffer.current.getCursor()[1], - getLineCount: () => buffer.current.getText().split("\n").length, - isCursorAtFirstRow: () => buffer.current.getCursor()[0] === 0, - isCursorAtLastRow: () => { - const [row] = buffer.current.getCursor(); - const lineCount = buffer.current.getText().split("\n").length; - return row === lineCount - 1; - }, - getText: () => buffer.current.getText(), - moveCursorToEnd: () => { - buffer.current.move("home"); - const lines = buffer.current.getText().split("\n"); - for (let i = 0; i < lines.length - 1; i++) { - buffer.current.move("down"); - } - buffer.current.move("end"); - // Force a re-render - setVersion((v) => v + 1); - }, - }), - [], - ); - - // Read everything from the buffer - const visibleLines = buffer.current.getVisibleLines({ - height, - width: effectiveWidth, - }); - const [cursorRow, cursorCol] = buffer.current.getCursor(); - const scrollRow = (buffer.current as any).scrollRow as number; - const scrollCol = (buffer.current as any).scrollCol as number; - - return ( - - {visibleLines.map((lineText, idx) => { - const absoluteRow = scrollRow + idx; - - // apply horizontal slice - let display = lineText.slice(scrollCol, scrollCol + effectiveWidth); - if (display.length < effectiveWidth) { - display = display.padEnd(effectiveWidth, " "); - } - - // Highlight the *character under the caret* (i.e. the one immediately - // to the right of the insertion position) so that the block cursor - // visually matches the logical caret location. This makes the - // highlighted glyph the one that would be replaced by `insert()` and - // *not* the one that would be removed by `backspace()`. - - if (absoluteRow === cursorRow) { - const relativeCol = cursorCol - scrollCol; - const highlightCol = relativeCol; - - if (highlightCol >= 0 && highlightCol < effectiveWidth) { - const charToHighlight = display[highlightCol] || " "; - const highlighted = chalk.inverse(charToHighlight); - display = - display.slice(0, highlightCol) + - highlighted + - display.slice(highlightCol + 1); - } else if (relativeCol === effectiveWidth) { - // Caret sits just past the right edge; show a block cursor in the - // gutter so the user still sees it. - display = display.slice(0, effectiveWidth - 1) + chalk.inverse(" "); - } - } - - return {display}; - })} - - ); -}; - -const MultilineTextEditor = React.forwardRef(MultilineTextEditorInner); -export default MultilineTextEditor; diff --git a/codex-cli/src/components/chat/terminal-chat-command-review.tsx b/codex-cli/src/components/chat/terminal-chat-command-review.tsx deleted file mode 100644 index 912af97961..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-command-review.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { ReviewDecision } from "../../utils/agent/review"; -// TODO: figure out why `cli-spinners` fails on Node v20.9.0 -// which is why we have to do this in the first place -// -// @ts-expect-error select.js is JavaScript and has no types -import { Select } from "../vendor/ink-select/select"; -import TextInput from "../vendor/ink-text-input"; -import { Box, Text, useInput } from "ink"; -import React from "react"; - -// default deny‑reason: -const DEFAULT_DENY_MESSAGE = - "Don't do that, but keep trying to fix the problem"; - -export function TerminalChatCommandReview({ - confirmationPrompt, - onReviewCommand, - // callback to switch approval mode overlay - onSwitchApprovalMode, - explanation: propExplanation, - // whether this review Select is active (listening for keys) - isActive = true, -}: { - confirmationPrompt: React.ReactNode; - onReviewCommand: (decision: ReviewDecision, customMessage?: string) => void; - onSwitchApprovalMode: () => void; - explanation?: string; - // when false, disable the underlying Select so it won't capture input - isActive?: boolean; -}): React.ReactElement { - const [mode, setMode] = React.useState<"select" | "input" | "explanation">( - "select", - ); - const [explanation, setExplanation] = React.useState(""); - - // If the component receives an explanation prop, update the state - React.useEffect(() => { - if (propExplanation) { - setExplanation(propExplanation); - setMode("explanation"); - } - }, [propExplanation]); - const [msg, setMsg] = React.useState(""); - - // ------------------------------------------------------------------------- - // Determine whether the "always approve" option should be displayed. We - // only hide it for the special `apply_patch` command since approving those - // permanently would bypass the user's review of future file modifications. - // The information is embedded in the `confirmationPrompt` React element – - // we inspect the `commandForDisplay` prop exposed by - // to extract the base command. - // ------------------------------------------------------------------------- - - const showAlwaysApprove = React.useMemo(() => { - if ( - React.isValidElement(confirmationPrompt) && - // eslint-disable-next-line @typescript-eslint/no-explicit-any - typeof (confirmationPrompt as any).props?.commandForDisplay === "string" - ) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const command: string = (confirmationPrompt as any).props - .commandForDisplay; - // Grab the first token of the first line – that corresponds to the base - // command even when the string contains embedded newlines (e.g. diffs). - const baseCmd = command.split("\n")[0]?.trim().split(/\s+/)[0] ?? ""; - return baseCmd !== "apply_patch"; - } - // Default to showing the option when we cannot reliably detect the base - // command. - return true; - }, [confirmationPrompt]); - - // Memoize the list of selectable options to avoid recreating the array on - // every render. This keeps { - if (value === "edit") { - setMode("input"); - } else if (value === "switch") { - onSwitchApprovalMode(); - } else { - onReviewCommand(value); - } - }} - options={approvalOptions} - /> - - - ) : mode === "input" ? ( - <> - Give the model feedback (↵ to submit): - - - - - - - {msg.trim() === "" && ( - - - default:  - {DEFAULT_DENY_MESSAGE} - - - )} - - ) : null} - - - ); -} diff --git a/codex-cli/src/components/chat/terminal-chat-completions.tsx b/codex-cli/src/components/chat/terminal-chat-completions.tsx deleted file mode 100644 index eb7e47f85f..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-completions.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Box, Text } from "ink"; -import React, { useMemo } from "react"; - -type TextCompletionProps = { - /** - * Array of text completion options to display in the list - */ - completions: Array; - - /** - * Maximum number of completion items to show at once in the view - */ - displayLimit: number; - - /** - * Index of the currently selected completion in the completions array - */ - selectedCompletion: number; -}; - -function TerminalChatCompletions({ - completions, - selectedCompletion, - displayLimit, -}: TextCompletionProps): JSX.Element { - const visibleItems = useMemo(() => { - // Try to keep selection centered in view - let startIndex = Math.max( - 0, - selectedCompletion - Math.floor(displayLimit / 2), - ); - - // Fix window position when at the end of the list - if (completions.length - startIndex < displayLimit) { - startIndex = Math.max(0, completions.length - displayLimit); - } - - const endIndex = Math.min(completions.length, startIndex + displayLimit); - - return completions.slice(startIndex, endIndex).map((completion, index) => ({ - completion, - originalIndex: index + startIndex, - })); - }, [completions, selectedCompletion, displayLimit]); - - return ( - - {visibleItems.map(({ completion, originalIndex }) => ( - - {completion} - - ))} - - ); -} - -export default TerminalChatCompletions; diff --git a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx deleted file mode 100644 index 714cc59fb3..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { log } from "../../utils/logger/log.js"; -import { Box, Text, useInput, useStdin } from "ink"; -import React, { useState } from "react"; -import { useInterval } from "use-interval"; - -// Retaining a single static placeholder text for potential future use. The -// more elaborate randomised thinking prompts were removed to streamline the -// UI – the elapsed‑time counter now provides sufficient feedback. - -export default function TerminalChatInputThinking({ - onInterrupt, - active, - thinkingSeconds, -}: { - onInterrupt: () => void; - active: boolean; - thinkingSeconds: number; -}): React.ReactElement { - const [awaitingConfirm, setAwaitingConfirm] = useState(false); - const [dots, setDots] = useState(""); - - // Animate the ellipsis - useInterval(() => { - setDots((prev) => (prev.length < 3 ? prev + "." : "")); - }, 500); - - const { stdin, setRawMode } = useStdin(); - - React.useEffect(() => { - if (!active) { - return; - } - - setRawMode?.(true); - - const onData = (data: Buffer | string) => { - if (awaitingConfirm) { - return; - } - - const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; - if (str === "\x1b\x1b") { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - setAwaitingConfirm(true); - setTimeout(() => setAwaitingConfirm(false), 1500); - } - }; - - stdin?.on("data", onData); - return () => { - stdin?.off("data", onData); - }; - }, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]); - - // No timers required beyond tracking the elapsed seconds supplied via props. - - useInput( - (_input, key) => { - if (!key.escape) { - return; - } - - if (awaitingConfirm) { - log("useInput: second ESC detected – triggering onInterrupt()"); - onInterrupt(); - setAwaitingConfirm(false); - } else { - log("useInput: first ESC detected – waiting for confirmation"); - setAwaitingConfirm(true); - setTimeout(() => setAwaitingConfirm(false), 1500); - } - }, - { isActive: active }, - ); - - // Custom ball animation including the elapsed seconds - const ballFrames = [ - "( ● )", - "( ● )", - "( ● )", - "( ● )", - "( ●)", - "( ● )", - "( ● )", - "( ● )", - "( ● )", - "(● )", - ]; - - const [frame, setFrame] = useState(0); - - useInterval(() => { - setFrame((idx) => (idx + 1) % ballFrames.length); - }, 80); - - // Preserve the spinner (ball) animation while keeping the elapsed seconds - // text static. We achieve this by rendering the bouncing ball inside the - // parentheses and appending the seconds counter *after* the spinner rather - // than injecting it directly next to the ball (which caused the counter to - // move horizontally together with the ball). - - const frameTemplate = ballFrames[frame] ?? ballFrames[0]; - const frameWithSeconds = `${frameTemplate} ${thinkingSeconds}s`; - - return ( - - - - {frameWithSeconds} - - Thinking - {dots} - - - - Press Esc twice to interrupt - - - {awaitingConfirm && ( - - Press Esc again to interrupt and enter a new - instruction - - )} - - ); -} diff --git a/codex-cli/src/components/chat/terminal-chat-input.tsx b/codex-cli/src/components/chat/terminal-chat-input.tsx deleted file mode 100644 index c8c5bf8216..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-input.tsx +++ /dev/null @@ -1,1017 +0,0 @@ -import type { MultilineTextEditorHandle } from "./multiline-editor"; -import type { ReviewDecision } from "../../utils/agent/review.js"; -import type { FileSystemSuggestion } from "../../utils/file-system-suggestions.js"; -import type { HistoryEntry } from "../../utils/storage/command-history.js"; -import type { - ResponseInputItem, - ResponseItem, -} from "openai/resources/responses/responses.mjs"; - -import MultilineTextEditor from "./multiline-editor"; -import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import TextCompletions from "./terminal-chat-completions.js"; -import { loadConfig } from "../../utils/config.js"; -import { getFileSystemSuggestions } from "../../utils/file-system-suggestions.js"; -import { expandFileTags } from "../../utils/file-tag-utils"; -import { createInputItem } from "../../utils/input-utils.js"; -import { log } from "../../utils/logger/log.js"; -import { setSessionId } from "../../utils/session.js"; -import { SLASH_COMMANDS, type SlashCommand } from "../../utils/slash-commands"; -import { - loadCommandHistory, - addToHistory, -} from "../../utils/storage/command-history.js"; -import { clearTerminal, onExit } from "../../utils/terminal.js"; -import { Box, Text, useApp, useInput, useStdin } from "ink"; -import { fileURLToPath } from "node:url"; -import React, { - useCallback, - useState, - Fragment, - useEffect, - useRef, -} from "react"; -import { useInterval } from "use-interval"; - -const suggestions = [ - "explain this codebase to me", - "fix any build errors", - "are there any bugs in my code?", -]; - -export default function TerminalChatInput({ - isNew, - loading, - submitInput, - confirmationPrompt, - explanation, - submitConfirmation, - setLastResponseId, - setItems, - contextLeftPercent, - openOverlay, - openModelOverlay, - openApprovalOverlay, - openHelpOverlay, - openDiffOverlay, - openSessionsOverlay, - onCompact, - interruptAgent, - active, - thinkingSeconds, - items = [], -}: { - isNew: boolean; - loading: boolean; - submitInput: (input: Array) => void; - confirmationPrompt: React.ReactNode | null; - explanation?: string; - submitConfirmation: ( - decision: ReviewDecision, - customDenyMessage?: string, - ) => void; - setLastResponseId: (lastResponseId: string) => void; - setItems: React.Dispatch>>; - contextLeftPercent: number; - openOverlay: () => void; - openModelOverlay: () => void; - openApprovalOverlay: () => void; - openHelpOverlay: () => void; - openDiffOverlay: () => void; - openSessionsOverlay: () => void; - onCompact: () => void; - interruptAgent: () => void; - active: boolean; - thinkingSeconds: number; - // New: current conversation items so we can include them in bug reports - items?: Array; -}): React.ReactElement { - // Slash command suggestion index - const [selectedSlashSuggestion, setSelectedSlashSuggestion] = - useState(0); - const app = useApp(); - const [selectedSuggestion, setSelectedSuggestion] = useState(0); - const [input, setInput] = useState(""); - const [history, setHistory] = useState>([]); - const [historyIndex, setHistoryIndex] = useState(null); - const [draftInput, setDraftInput] = useState(""); - const [skipNextSubmit, setSkipNextSubmit] = useState(false); - const [fsSuggestions, setFsSuggestions] = useState< - Array - >([]); - const [selectedCompletion, setSelectedCompletion] = useState(-1); - // Multiline text editor key to force remount after submission - const [editorState, setEditorState] = useState<{ - key: number; - initialCursorOffset?: number; - }>({ key: 0 }); - // Imperative handle from the multiline editor so we can query caret position - const editorRef = useRef(null); - // Track the caret row across keystrokes - const prevCursorRow = useRef(null); - const prevCursorWasAtLastRow = useRef(false); - - // --- Helper for updating input, remounting editor, and moving cursor to end --- - const applyFsSuggestion = useCallback((newInputText: string) => { - setInput(newInputText); - setEditorState((s) => ({ - key: s.key + 1, - initialCursorOffset: newInputText.length, - })); - }, []); - - // --- Helper for updating file system suggestions --- - function updateFsSuggestions( - txt: string, - alwaysUpdateSelection: boolean = false, - ) { - // Clear file system completions if a space is typed - if (txt.endsWith(" ")) { - setFsSuggestions([]); - setSelectedCompletion(-1); - } else { - // Determine the current token (last whitespace-separated word) - const words = txt.trim().split(/\s+/); - const lastWord = words[words.length - 1] ?? ""; - - const shouldUpdateSelection = - lastWord.startsWith("@") || alwaysUpdateSelection; - - // Strip optional leading '@' for the path prefix - let pathPrefix: string; - if (lastWord.startsWith("@")) { - pathPrefix = lastWord.slice(1); - // If only '@' is typed, list everything in the current directory - pathPrefix = pathPrefix.length === 0 ? "./" : pathPrefix; - } else { - pathPrefix = lastWord; - } - - if (shouldUpdateSelection) { - const completions = getFileSystemSuggestions(pathPrefix); - setFsSuggestions(completions); - if (completions.length > 0) { - setSelectedCompletion((prev) => - prev < 0 || prev >= completions.length ? 0 : prev, - ); - } else { - setSelectedCompletion(-1); - } - } else if (fsSuggestions.length > 0) { - // Token cleared → clear menu - setFsSuggestions([]); - setSelectedCompletion(-1); - } - } - } - - /** - * Result of replacing text with a file system suggestion - */ - interface ReplacementResult { - /** The new text with the suggestion applied */ - text: string; - /** The selected suggestion if a replacement was made */ - suggestion: FileSystemSuggestion | null; - /** Whether a replacement was actually made */ - wasReplaced: boolean; - } - - // --- Helper for replacing input with file system suggestion --- - function getFileSystemSuggestion( - txt: string, - requireAtPrefix: boolean = false, - ): ReplacementResult { - if (fsSuggestions.length === 0 || selectedCompletion < 0) { - return { text: txt, suggestion: null, wasReplaced: false }; - } - - const words = txt.trim().split(/\s+/); - const lastWord = words[words.length - 1] ?? ""; - - // Check if @ prefix is required and the last word doesn't have it - if (requireAtPrefix && !lastWord.startsWith("@")) { - return { text: txt, suggestion: null, wasReplaced: false }; - } - - const selected = fsSuggestions[selectedCompletion]; - if (!selected) { - return { text: txt, suggestion: null, wasReplaced: false }; - } - - const replacement = lastWord.startsWith("@") - ? `@${selected.path}` - : selected.path; - words[words.length - 1] = replacement; - return { - text: words.join(" "), - suggestion: selected, - wasReplaced: true, - }; - } - - // Load command history on component mount - useEffect(() => { - async function loadHistory() { - const historyEntries = await loadCommandHistory(); - setHistory(historyEntries); - } - - loadHistory(); - }, []); - // Reset slash suggestion index when input prefix changes - useEffect(() => { - if (input.trim().startsWith("/")) { - setSelectedSlashSuggestion(0); - } - }, [input]); - - useInput( - (_input, _key) => { - // Slash command navigation: up/down to select, enter to fill - if (!confirmationPrompt && !loading && input.trim().startsWith("/")) { - const prefix = input.trim(); - const matches = SLASH_COMMANDS.filter((cmd: SlashCommand) => - cmd.command.startsWith(prefix), - ); - if (matches.length > 0) { - if (_key.tab) { - // Cycle and fill slash command suggestions on Tab - const len = matches.length; - // Determine new index based on shift state - const nextIdx = _key.shift - ? selectedSlashSuggestion <= 0 - ? len - 1 - : selectedSlashSuggestion - 1 - : selectedSlashSuggestion >= len - 1 - ? 0 - : selectedSlashSuggestion + 1; - setSelectedSlashSuggestion(nextIdx); - // Autocomplete the command in the input - const match = matches[nextIdx]; - if (!match) { - return; - } - const cmd = match.command; - setInput(cmd); - setDraftInput(cmd); - return; - } - if (_key.upArrow) { - setSelectedSlashSuggestion((prev) => - prev <= 0 ? matches.length - 1 : prev - 1, - ); - return; - } - if (_key.downArrow) { - setSelectedSlashSuggestion((prev) => - prev < 0 || prev >= matches.length - 1 ? 0 : prev + 1, - ); - return; - } - if (_key.return) { - // Execute the currently selected slash command - const selIdx = selectedSlashSuggestion; - const cmdObj = matches[selIdx]; - if (cmdObj) { - const cmd = cmdObj.command; - setInput(""); - setDraftInput(""); - setSelectedSlashSuggestion(0); - switch (cmd) { - case "/history": - openOverlay(); - break; - case "/sessions": - openSessionsOverlay(); - break; - case "/help": - openHelpOverlay(); - break; - case "/compact": - onCompact(); - break; - case "/model": - openModelOverlay(); - break; - case "/approval": - openApprovalOverlay(); - break; - case "/diff": - openDiffOverlay(); - break; - case "/bug": - onSubmit(cmd); - break; - case "/clear": - onSubmit(cmd); - break; - case "/clearhistory": - onSubmit(cmd); - break; - default: - break; - } - } - return; - } - } - } - if (!confirmationPrompt && !loading) { - if (fsSuggestions.length > 0) { - if (_key.upArrow) { - setSelectedCompletion((prev) => - prev <= 0 ? fsSuggestions.length - 1 : prev - 1, - ); - return; - } - - if (_key.downArrow) { - setSelectedCompletion((prev) => - prev >= fsSuggestions.length - 1 ? 0 : prev + 1, - ); - return; - } - - if (_key.tab && selectedCompletion >= 0) { - const { text: newText, wasReplaced } = - getFileSystemSuggestion(input); - - // Only proceed if the text was actually changed - if (wasReplaced) { - applyFsSuggestion(newText); - setFsSuggestions([]); - setSelectedCompletion(-1); - } - return; - } - } - - if (_key.upArrow) { - let moveThroughHistory = true; - - // Only use history when the caret was *already* on the very first - // row *before* this key-press. - const cursorRow = editorRef.current?.getRow?.() ?? 0; - const cursorCol = editorRef.current?.getCol?.() ?? 0; - const wasAtFirstRow = (prevCursorRow.current ?? cursorRow) === 0; - if (!(cursorRow === 0 && wasAtFirstRow)) { - moveThroughHistory = false; - } - - // If we are not yet in history mode, then also require that the col is zero so that - // we only trigger history navigation when the user is at the start of the input. - if (historyIndex == null && !(cursorRow === 0 && cursorCol === 0)) { - moveThroughHistory = false; - } - - // Move through history. - if (history.length && moveThroughHistory) { - let newIndex: number; - if (historyIndex == null) { - const currentDraft = editorRef.current?.getText?.() ?? input; - setDraftInput(currentDraft); - newIndex = history.length - 1; - } else { - newIndex = Math.max(0, historyIndex - 1); - } - setHistoryIndex(newIndex); - - setInput(history[newIndex]?.command ?? ""); - // Re-mount the editor so it picks up the new initialText - setEditorState((s) => ({ key: s.key + 1 })); - return; // handled - } - - // Otherwise let it propagate. - } - - if (_key.downArrow) { - // Only move forward in history when we're already *in* history mode - // AND the caret sits on the last line of the buffer. - const wasAtLastRow = - prevCursorWasAtLastRow.current ?? - editorRef.current?.isCursorAtLastRow() ?? - true; - if (historyIndex != null && wasAtLastRow) { - const newIndex = historyIndex + 1; - if (newIndex >= history.length) { - setHistoryIndex(null); - setInput(draftInput); - setEditorState((s) => ({ key: s.key + 1 })); - } else { - setHistoryIndex(newIndex); - setInput(history[newIndex]?.command ?? ""); - setEditorState((s) => ({ key: s.key + 1 })); - } - return; // handled - } - // Otherwise let it propagate - } - - // Defer filesystem suggestion logic to onSubmit if enter key is pressed - if (!_key.return) { - // Pressing tab should trigger the file system suggestions - const shouldUpdateSelection = _key.tab; - const targetInput = _key.delete ? input.slice(0, -1) : input + _input; - updateFsSuggestions(targetInput, shouldUpdateSelection); - } - } - - // Update the cached cursor position *after* **all** handlers (including - // the internal ) have processed this key event. - // - // Ink invokes `useInput` callbacks starting with **parent** components - // first, followed by their descendants. As a result the call above - // executes *before* the editor has had a chance to react to the key - // press and update its internal caret position. When navigating - // through a multi-line draft with the ↑ / ↓ arrow keys this meant we - // recorded the *old* cursor row instead of the one that results *after* - // the key press. Consequently, a subsequent ↑ still saw - // `prevCursorRow = 1` even though the caret was already on row 0 and - // history-navigation never kicked in. - // - // Defer the sampling by one tick so we read the *final* caret position - // for this frame. - setTimeout(() => { - prevCursorRow.current = editorRef.current?.getRow?.() ?? null; - prevCursorWasAtLastRow.current = - editorRef.current?.isCursorAtLastRow?.() ?? true; - }, 1); - - if (input.trim() === "" && isNew) { - if (_key.tab) { - setSelectedSuggestion( - (s) => (s + (_key.shift ? -1 : 1)) % (suggestions.length + 1), - ); - } else if (selectedSuggestion && _key.return) { - const suggestion = suggestions[selectedSuggestion - 1] || ""; - setInput(""); - setSelectedSuggestion(0); - submitInput([ - { - role: "user", - content: [{ type: "input_text", text: suggestion }], - type: "message", - }, - ]); - } - } else if (_input === "\u0003" || (_input === "c" && _key.ctrl)) { - setTimeout(() => { - app.exit(); - onExit(); - process.exit(0); - }, 60); - } - }, - { isActive: active }, - ); - - const onSubmit = useCallback( - async (value: string) => { - const inputValue = value.trim(); - - // If the user only entered a slash, do not send a chat message. - if (inputValue === "/") { - setInput(""); - return; - } - - // Skip this submit if we just autocompleted a slash command. - if (skipNextSubmit) { - setSkipNextSubmit(false); - return; - } - - if (!inputValue) { - return; - } else if (inputValue === "/history") { - setInput(""); - openOverlay(); - return; - } else if (inputValue === "/sessions") { - setInput(""); - openSessionsOverlay(); - return; - } else if (inputValue === "/help") { - setInput(""); - openHelpOverlay(); - return; - } else if (inputValue === "/diff") { - setInput(""); - openDiffOverlay(); - return; - } else if (inputValue === "/compact") { - setInput(""); - onCompact(); - return; - } else if (inputValue.startsWith("/model")) { - setInput(""); - openModelOverlay(); - return; - } else if (inputValue.startsWith("/approval")) { - setInput(""); - openApprovalOverlay(); - return; - } else if (["exit", "q", ":q"].includes(inputValue)) { - setInput(""); - setTimeout(() => { - app.exit(); - onExit(); - process.exit(0); - }, 60); // Wait one frame. - return; - } else if (inputValue === "/clear" || inputValue === "clear") { - setInput(""); - setSessionId(""); - setLastResponseId(""); - - // Clear the terminal screen (including scrollback) before resetting context. - clearTerminal(); - - // Emit a system message to confirm the clear action. We *append* - // it so Ink's treats it as new output and actually renders it. - setItems((prev) => { - const filteredOldItems = prev.filter((item) => { - // Remove any token‑heavy entries (user/assistant turns and function calls) - if ( - item.type === "message" && - (item.role === "user" || item.role === "assistant") - ) { - return false; - } - if ( - item.type === "function_call" || - item.type === "function_call_output" - ) { - return false; - } - return true; // keep developer/system and other meta entries - }); - - return [ - ...filteredOldItems, - { - id: `clear-${Date.now()}`, - type: "message", - role: "system", - content: [{ type: "input_text", text: "Terminal cleared" }], - }, - ]; - }); - - return; - } else if (inputValue === "/clearhistory") { - setInput(""); - - // Import clearCommandHistory function to avoid circular dependencies - // Using dynamic import to lazy-load the function - import("../../utils/storage/command-history.js").then( - async ({ clearCommandHistory }) => { - await clearCommandHistory(); - setHistory([]); - - // Emit a system message to confirm the history clear action. - setItems((prev) => [ - ...prev, - { - id: `clearhistory-${Date.now()}`, - type: "message", - role: "system", - content: [ - { type: "input_text", text: "Command history cleared" }, - ], - }, - ]); - }, - ); - - return; - } else if (inputValue === "/bug") { - // Generate a GitHub bug report URL pre‑filled with session details. - setInput(""); - - try { - const os = await import("node:os"); - const { CLI_VERSION } = await import("../../version.js"); - const { buildBugReportUrl } = await import( - "../../utils/bug-report.js" - ); - - const url = buildBugReportUrl({ - items: items ?? [], - cliVersion: CLI_VERSION, - model: loadConfig().model ?? "unknown", - platform: [os.platform(), os.arch(), os.release()] - .map((s) => `\`${s}\``) - .join(" | "), - }); - - setItems((prev) => [ - ...prev, - { - id: `bugreport-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `🔗 Bug report URL: ${url}`, - }, - ], - }, - ]); - } catch (error) { - // If anything went wrong, notify the user. - setItems((prev) => [ - ...prev, - { - id: `bugreport-error-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `⚠️ Failed to create bug report URL: ${error}`, - }, - ], - }, - ]); - } - - return; - } else if (inputValue.startsWith("/")) { - // Handle invalid/unrecognized commands. Only single-word inputs starting with '/' - // (e.g., /command) that are not recognized are caught here. Any other input, including - // those starting with '/' but containing spaces (e.g., "/command arg"), will fall through - // and be treated as a regular prompt. - const trimmed = inputValue.trim(); - - if (/^\/\S+$/.test(trimmed)) { - setInput(""); - setItems((prev) => [ - ...prev, - { - id: `invalidcommand-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Invalid command "${trimmed}". Use /help to retrieve the list of commands.`, - }, - ], - }, - ]); - - return; - } - } - - // detect image file paths for dynamic inclusion - const images: Array = []; - let text = inputValue; - - // markdown-style image syntax: ![alt](path) - text = text.replace(/!\[[^\]]*?\]\(([^)]+)\)/g, (_m, p1: string) => { - images.push(p1.startsWith("file://") ? fileURLToPath(p1) : p1); - return ""; - }); - - // quoted file paths ending with common image extensions (e.g. '/path/to/img.png') - text = text.replace( - /['"]([^'"]+?\.(?:png|jpe?g|gif|bmp|webp|svg))['"]/gi, - (_m, p1: string) => { - images.push(p1.startsWith("file://") ? fileURLToPath(p1) : p1); - return ""; - }, - ); - - // bare file paths ending with common image extensions - text = text.replace( - // eslint-disable-next-line no-useless-escape - /\b(?:\.[\/\\]|[\/\\]|[A-Za-z]:[\/\\])?[\w-]+(?:[\/\\][\w-]+)*\.(?:png|jpe?g|gif|bmp|webp|svg)\b/gi, - (match: string) => { - images.push( - match.startsWith("file://") ? fileURLToPath(match) : match, - ); - return ""; - }, - ); - text = text.trim(); - - // Expand @file tokens into XML blocks for the model - const expandedText = await expandFileTags(text); - - const inputItem = await createInputItem(expandedText, images); - submitInput([inputItem]); - - // Get config for history persistence. - const config = loadConfig(); - - // Add to history and update state. - const updatedHistory = await addToHistory(value, history, { - maxSize: config.history?.maxSize ?? 1000, - saveHistory: config.history?.saveHistory ?? true, - sensitivePatterns: config.history?.sensitivePatterns ?? [], - }); - - setHistory(updatedHistory); - setHistoryIndex(null); - setDraftInput(""); - setSelectedSuggestion(0); - setInput(""); - setFsSuggestions([]); - setSelectedCompletion(-1); - }, - [ - setInput, - submitInput, - setLastResponseId, - setItems, - app, - setHistory, - setHistoryIndex, - openOverlay, - openApprovalOverlay, - openModelOverlay, - openHelpOverlay, - openDiffOverlay, - openSessionsOverlay, - history, - onCompact, - skipNextSubmit, - items, - ], - ); - - if (confirmationPrompt) { - return ( - - ); - } - - return ( - - - {loading ? ( - - ) : ( - - { - setDraftInput(txt); - if (historyIndex != null) { - setHistoryIndex(null); - } - setInput(txt); - }} - key={editorState.key} - initialCursorOffset={editorState.initialCursorOffset} - initialText={input} - height={6} - focus={active} - onSubmit={(txt) => { - // If final token is an @path, replace with filesystem suggestion if available - const { - text: replacedText, - suggestion, - wasReplaced, - } = getFileSystemSuggestion(txt, true); - - // If we replaced @path token with a directory, don't submit - if (wasReplaced && suggestion?.isDirectory) { - applyFsSuggestion(replacedText); - // Update suggestions for the new directory - updateFsSuggestions(replacedText, true); - return; - } - - onSubmit(replacedText); - setEditorState((s) => ({ key: s.key + 1 })); - setInput(""); - setHistoryIndex(null); - setDraftInput(""); - }} - /> - - )} - - {/* Slash command autocomplete suggestions */} - {input.trim().startsWith("/") && ( - - {SLASH_COMMANDS.filter((cmd: SlashCommand) => - cmd.command.startsWith(input.trim()), - ).map((cmd: SlashCommand, idx: number) => ( - - - {cmd.command} - {cmd.description} - - - ))} - - )} - - {isNew && !input ? ( - - try:{" "} - {suggestions.map((m, key) => ( - - {key !== 0 ? " | " : ""} - - {m} - - - ))} - - ) : fsSuggestions.length > 0 ? ( - suggestion.path)} - selectedCompletion={selectedCompletion} - displayLimit={5} - /> - ) : ( - - ctrl+c to exit | "/" to see commands | enter to send - {contextLeftPercent > 25 && ( - <> - {" — "} - 40 ? "green" : "yellow"}> - {Math.round(contextLeftPercent)}% context left - - - )} - {contextLeftPercent <= 25 && ( - <> - {" — "} - - {Math.round(contextLeftPercent)}% context left — send - "/compact" to condense context - - - )} - - )} - - - ); -} - -function TerminalChatInputThinking({ - onInterrupt, - active, - thinkingSeconds, -}: { - onInterrupt: () => void; - active: boolean; - thinkingSeconds: number; -}) { - const [awaitingConfirm, setAwaitingConfirm] = useState(false); - const [dots, setDots] = useState(""); - - // Animate ellipsis - useInterval(() => { - setDots((prev) => (prev.length < 3 ? prev + "." : "")); - }, 500); - - // Spinner frames with embedded seconds - const ballFrames = [ - "( ● )", - "( ● )", - "( ● )", - "( ● )", - "( ●)", - "( ● )", - "( ● )", - "( ● )", - "( ● )", - "(● )", - ]; - const [frame, setFrame] = useState(0); - - useInterval(() => { - setFrame((idx) => (idx + 1) % ballFrames.length); - }, 80); - - // Keep the elapsed‑seconds text fixed while the ball animation moves. - const frameTemplate = ballFrames[frame] ?? ballFrames[0]; - const frameWithSeconds = `${frameTemplate} ${thinkingSeconds}s`; - - // --------------------------------------------------------------------- - // Raw stdin listener to catch the case where the terminal delivers two - // consecutive ESC bytes ("\x1B\x1B") in a *single* chunk. Ink's `useInput` - // collapses that sequence into one key event, so the regular two‑step - // handler above never sees the second press. By inspecting the raw data - // we can identify this special case and trigger the interrupt while still - // requiring a double press for the normal single‑byte ESC events. - // --------------------------------------------------------------------- - - const { stdin, setRawMode } = useStdin(); - - React.useEffect(() => { - if (!active) { - return; - } - - // Ensure raw mode – already enabled by Ink when the component has focus, - // but called defensively in case that assumption ever changes. - setRawMode?.(true); - - const onData = (data: Buffer | string) => { - if (awaitingConfirm) { - return; // already awaiting a second explicit press - } - - // Handle both Buffer and string forms. - const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; - if (str === "\x1b\x1b") { - // Treat as the first Escape press – prompt the user for confirmation. - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - setAwaitingConfirm(true); - setTimeout(() => setAwaitingConfirm(false), 1500); - } - }; - - stdin?.on("data", onData); - - return () => { - stdin?.off("data", onData); - }; - }, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]); - - // No local timer: the parent component supplies the elapsed time via props. - - // Listen for the escape key to allow the user to interrupt the current - // operation. We require two presses within a short window (1.5s) to avoid - // accidental cancellations. - useInput( - (_input, key) => { - if (!key.escape) { - return; - } - - if (awaitingConfirm) { - log("useInput: second ESC detected – triggering onInterrupt()"); - onInterrupt(); - setAwaitingConfirm(false); - } else { - log("useInput: first ESC detected – waiting for confirmation"); - setAwaitingConfirm(true); - setTimeout(() => setAwaitingConfirm(false), 1500); - } - }, - { isActive: active }, - ); - - return ( - - - - {frameWithSeconds} - - Thinking - {dots} - - - - press Esc{" "} - {awaitingConfirm ? ( - again - ) : ( - twice - )}{" "} - to interrupt - - - - ); -} diff --git a/codex-cli/src/components/chat/terminal-chat-past-rollout.tsx b/codex-cli/src/components/chat/terminal-chat-past-rollout.tsx deleted file mode 100644 index 1ac8280edb..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-past-rollout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import type { TerminalChatSession } from "../../utils/session.js"; -import type { ResponseItem } from "openai/resources/responses/responses"; -import type { FileOpenerScheme } from "src/utils/config.js"; - -import TerminalChatResponseItem from "./terminal-chat-response-item"; -import { Box, Text } from "ink"; -import React from "react"; - -export default function TerminalChatPastRollout({ - session, - items, - fileOpener, -}: { - session: TerminalChatSession; - items: Array; - fileOpener: FileOpenerScheme | undefined; -}): React.ReactElement { - const { version, id: sessionId, model } = session; - return ( - - - - ● OpenAI Codex{" "} - - (research preview) v{version} - - - - - - ● localhost{" "} - · session:{" "} - - {sessionId} - - - - ↳ When / Who:{" "} - - {session.timestamp} / {session.user} - - - - ↳ model: {model} - - - - {React.useMemo( - () => - items.map((item, key) => ( - - )), - [items, fileOpener], - )} - - - ); -} diff --git a/codex-cli/src/components/chat/terminal-chat-response-item.tsx b/codex-cli/src/components/chat/terminal-chat-response-item.tsx deleted file mode 100644 index bab4aa317f..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-response-item.tsx +++ /dev/null @@ -1,360 +0,0 @@ -import type { OverlayModeType } from "./terminal-chat"; -import type { TerminalRendererOptions } from "marked-terminal"; -import type { - ResponseFunctionToolCallItem, - ResponseFunctionToolCallOutputItem, - ResponseInputMessageItem, - ResponseItem, - ResponseOutputMessage, - ResponseReasoningItem, -} from "openai/resources/responses/responses"; -import type { FileOpenerScheme } from "src/utils/config"; - -import { useTerminalSize } from "../../hooks/use-terminal-size"; -import { collapseXmlBlocks } from "../../utils/file-tag-utils"; -import { parseToolCall, parseToolCallOutput } from "../../utils/parsers"; -import chalk, { type ForegroundColorName } from "chalk"; -import { Box, Text } from "ink"; -import { parse, setOptions } from "marked"; -import TerminalRenderer from "marked-terminal"; -import path from "path"; -import React, { useEffect, useMemo } from "react"; -import { formatCommandForDisplay } from "src/format-command.js"; -import supportsHyperlinks from "supports-hyperlinks"; - -export default function TerminalChatResponseItem({ - item, - fullStdout = false, - setOverlayMode, - fileOpener, -}: { - item: ResponseItem; - fullStdout?: boolean; - setOverlayMode?: React.Dispatch>; - fileOpener: FileOpenerScheme | undefined; -}): React.ReactElement { - switch (item.type) { - case "message": - return ( - - ); - // @ts-expect-error new item types aren't in SDK yet - case "local_shell_call": - case "function_call": - return ; - // @ts-expect-error new item types aren't in SDK yet - case "local_shell_call_output": - case "function_call_output": - return ( - - ); - default: - break; - } - - // @ts-expect-error `reasoning` is not in the responses API yet - if (item.type === "reasoning") { - return ( - - ); - } - - return ; -} - -// TODO: this should be part of `ResponseReasoningItem`. Also it doesn't work. -// --------------------------------------------------------------------------- -// Utility helpers -// --------------------------------------------------------------------------- - -/** - * Guess how long the assistant spent "thinking" based on the combined length - * of the reasoning summary. The calculation itself is fast, but wrapping it in - * `useMemo` in the consuming component ensures it only runs when the - * `summary` array actually changes. - */ -// TODO: use actual thinking time -// -// function guessThinkingTime(summary: Array) { -// const totalTextLength = summary -// .map((t) => t.text.length) -// .reduce((a, b) => a + b, summary.length - 1); -// return Math.max(1, Math.ceil(totalTextLength / 300)); -// } - -export function TerminalChatResponseReasoning({ - message, - fileOpener, -}: { - message: ResponseReasoningItem & { duration_ms?: number }; - fileOpener: FileOpenerScheme | undefined; -}): React.ReactElement | null { - // Only render when there is a reasoning summary - if (!message.summary || message.summary.length === 0) { - return null; - } - return ( - - {message.summary.map((summary, key) => { - const s = summary as { headline?: string; text: string }; - return ( - - {s.headline && {s.headline}} - {s.text} - - ); - })} - - ); -} - -const colorsByRole: Record = { - assistant: "magentaBright", - user: "blueBright", -}; - -function TerminalChatResponseMessage({ - message, - setOverlayMode, - fileOpener, -}: { - message: ResponseInputMessageItem | ResponseOutputMessage; - setOverlayMode?: React.Dispatch>; - fileOpener: FileOpenerScheme | undefined; -}) { - // auto switch to model mode if the system message contains "has been deprecated" - useEffect(() => { - if (message.role === "system") { - const systemMessage = message.content.find( - (c) => c.type === "input_text", - )?.text; - if (systemMessage?.includes("model_not_found")) { - setOverlayMode?.("model"); - } - } - }, [message, setOverlayMode]); - - return ( - - - {message.role === "assistant" ? "codex" : message.role} - - - {message.content - .map( - (c) => - c.type === "output_text" - ? c.text - : c.type === "refusal" - ? c.refusal - : c.type === "input_text" - ? collapseXmlBlocks(c.text) - : c.type === "input_image" - ? "" - : c.type === "input_file" - ? c.filename - : "", // unknown content type - ) - .join(" ")} - - - ); -} - -function TerminalChatResponseToolCall({ - message, -}: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - message: ResponseFunctionToolCallItem | any; -}) { - let workdir: string | undefined; - let cmdReadableText: string | undefined; - if (message.type === "function_call") { - const details = parseToolCall(message); - workdir = details?.workdir; - cmdReadableText = details?.cmdReadableText; - } else if (message.type === "local_shell_call") { - const action = message.action; - workdir = action.working_directory; - cmdReadableText = formatCommandForDisplay(action.command); - } - return ( - - - command - {workdir ? {` (${workdir})`} : ""} - - - $ {cmdReadableText} - - - ); -} - -function TerminalChatResponseToolCallOutput({ - message, - fullStdout, -}: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - message: ResponseFunctionToolCallOutputItem | any; - fullStdout: boolean; -}) { - const { output, metadata } = parseToolCallOutput(message.output); - const { exit_code, duration_seconds } = metadata; - const metadataInfo = useMemo( - () => - [ - typeof exit_code !== "undefined" ? `code: ${exit_code}` : "", - typeof duration_seconds !== "undefined" - ? `duration: ${duration_seconds}s` - : "", - ] - .filter(Boolean) - .join(", "), - [exit_code, duration_seconds], - ); - let displayedContent = output; - if (message.type === "function_call_output" && !fullStdout) { - const lines = displayedContent.split("\n"); - if (lines.length > 4) { - const head = lines.slice(0, 4); - const remaining = lines.length - 4; - displayedContent = [...head, `... (${remaining} more lines)`].join("\n"); - } - } - - // ------------------------------------------------------------------------- - // Colorize diff output: lines starting with '-' in red, '+' in green. - // This makes patches and other diff‑like stdout easier to read. - // We exclude the typical diff file headers ('---', '+++') so they retain - // the default color. This is a best‑effort heuristic and should be safe for - // non‑diff output – only the very first character of a line is inspected. - // ------------------------------------------------------------------------- - const colorizedContent = displayedContent - .split("\n") - .map((line) => { - if (line.startsWith("+") && !line.startsWith("++")) { - return chalk.green(line); - } - if (line.startsWith("-") && !line.startsWith("--")) { - return chalk.red(line); - } - return line; - }) - .join("\n"); - return ( - - - command.stdout{" "} - {metadataInfo ? `(${metadataInfo})` : ""} - - {colorizedContent} - - ); -} - -export function TerminalChatResponseGenericMessage({ - message, -}: { - message: ResponseItem; -}): React.ReactElement { - return {JSON.stringify(message, null, 2)}; -} - -export type MarkdownProps = TerminalRendererOptions & { - children: string; - fileOpener: FileOpenerScheme | undefined; - /** Base path for resolving relative file citation paths. */ - cwd?: string; -}; - -export function Markdown({ - children, - fileOpener, - cwd, - ...options -}: MarkdownProps): React.ReactElement { - const size = useTerminalSize(); - - const rendered = React.useMemo(() => { - const linkifiedMarkdown = rewriteFileCitations(children, fileOpener, cwd); - - // Configure marked for this specific render - setOptions({ - // @ts-expect-error missing parser, space props - renderer: new TerminalRenderer({ ...options, width: size.columns }), - }); - const parsed = parse(linkifiedMarkdown, { async: false }).trim(); - - // Remove the truncation logic - return parsed; - // eslint-disable-next-line react-hooks/exhaustive-deps -- options is an object of primitives - }, [ - children, - size.columns, - size.rows, - fileOpener, - supportsHyperlinks.stdout, - chalk.level, - ]); - - return {rendered}; -} - -/** Regex to match citations for source files (hence the `F:` prefix). */ -const citationRegex = new RegExp( - [ - // Opening marker - "【", - - // Capture group 1: file ID or name (anything except '†') - "F:([^†]+)", - - // Field separator - "†", - - // Capture group 2: start line (digits) - "L(\\d+)", - - // Non-capturing group for optional end line - "(?:", - - // Capture group 3: end line (digits or '?') - "-L(\\d+|\\?)", - - // End of optional group (may not be present) - ")?", - - // Closing marker - "】", - ].join(""), - "g", // Global flag -); - -function rewriteFileCitations( - markdown: string, - fileOpener: FileOpenerScheme | undefined, - cwd: string = process.cwd(), -): string { - citationRegex.lastIndex = 0; - return markdown.replace(citationRegex, (_match, file, start, _end) => { - const absPath = path.resolve(cwd, file); - if (!fileOpener) { - return `[${file}](${absPath})`; - } - const uri = `${fileOpener}://file${absPath}:${start}`; - const label = `${file}:${start}`; - // In practice, sometimes multiple citations for the same file, but with a - // different line number, are shown sequentially, so we: - // - include the line number in the label to disambiguate them - // - add a space after the link to make it easier to read - return `[${label}](${uri}) `; - }); -} diff --git a/codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx b/codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx deleted file mode 100644 index 614ebf382f..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { parseApplyPatch } from "../../parse-apply-patch"; -import { shortenPath } from "../../utils/short-path"; -import chalk from "chalk"; -import { Text } from "ink"; -import React from "react"; - -export function TerminalChatToolCallCommand({ - commandForDisplay, - explanation, -}: { - commandForDisplay: string; - explanation?: string; -}): React.ReactElement { - // ------------------------------------------------------------------------- - // Colorize diff output inside the command preview: we detect individual - // lines that begin with '+' or '-' (excluding the typical diff headers like - // '+++', '---', '++', '--') and apply green/red coloring. This mirrors - // how Git shows diffs and makes the patch easier to review. - // ------------------------------------------------------------------------- - - const colorizedCommand = commandForDisplay - .split("\n") - .map((line) => { - if (line.startsWith("+") && !line.startsWith("++")) { - return chalk.green(line); - } - if (line.startsWith("-") && !line.startsWith("--")) { - return chalk.red(line); - } - return line; - }) - .join("\n"); - - return ( - <> - - Shell Command - - - $ {colorizedCommand} - - {explanation && ( - <> - - Explanation - - {explanation.split("\n").map((line, i) => { - // Apply different styling to headings (numbered items) - if (line.match(/^\d+\.\s+/)) { - return ( - - {line} - - ); - } else if (line.match(/^\s*\*\s+/)) { - // Style bullet points - return ( - - {line} - - ); - } else if (line.match(/^(WARNING|CAUTION|NOTE):/i)) { - // Style warnings - return ( - - {line} - - ); - } else { - return {line}; - } - })} - - )} - - ); -} - -export function TerminalChatToolCallApplyPatch({ - commandForDisplay, - patch, -}: { - commandForDisplay: string; - patch: string; -}): React.ReactElement { - const ops = React.useMemo(() => parseApplyPatch(patch), [patch]); - const firstOp = ops?.[0]; - - const title = React.useMemo(() => { - if (!firstOp) { - return ""; - } - return capitalize(firstOp.type); - }, [firstOp]); - - const filePath = React.useMemo(() => { - if (!firstOp) { - return ""; - } - return shortenPath(firstOp.path || "."); - }, [firstOp]); - - if (ops == null) { - return ( - <> - - Invalid Patch - - - The provided patch command is invalid. - - {commandForDisplay} - - ); - } - - if (!firstOp) { - return ( - <> - - Empty Patch - - - No operations found in the patch command. - - {commandForDisplay} - - ); - } - - return ( - <> - - {title} {filePath} - - - $ {commandForDisplay} - - - ); -} - -const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx deleted file mode 100644 index d41a94990f..0000000000 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ /dev/null @@ -1,766 +0,0 @@ -import type { AppRollout } from "../../app.js"; -import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; -import type { CommandConfirmation } from "../../utils/agent/agent-loop.js"; -import type { AppConfig } from "../../utils/config.js"; -import type { ColorName } from "chalk"; -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; - -import TerminalChatInput from "./terminal-chat-input.js"; -import TerminalChatPastRollout from "./terminal-chat-past-rollout.js"; -import { TerminalChatToolCallCommand } from "./terminal-chat-tool-call-command.js"; -import TerminalMessageHistory from "./terminal-message-history.js"; -import { formatCommandForDisplay } from "../../format-command.js"; -import { useConfirmation } from "../../hooks/use-confirmation.js"; -import { useTerminalSize } from "../../hooks/use-terminal-size.js"; -import { AgentLoop } from "../../utils/agent/agent-loop.js"; -import { ReviewDecision } from "../../utils/agent/review.js"; -import { generateCompactSummary } from "../../utils/compact-summary.js"; -import { saveConfig } from "../../utils/config.js"; -import { extractAppliedPatches as _extractAppliedPatches } from "../../utils/extract-applied-patches.js"; -import { getGitDiff } from "../../utils/get-diff.js"; -import { createInputItem } from "../../utils/input-utils.js"; -import { log } from "../../utils/logger/log.js"; -import { - getAvailableModels, - calculateContextPercentRemaining, - uniqueById, -} from "../../utils/model-utils.js"; -import { createOpenAIClient } from "../../utils/openai-client.js"; -import { shortCwd } from "../../utils/short-path.js"; -import { saveRollout } from "../../utils/storage/save-rollout.js"; -import { CLI_VERSION } from "../../version.js"; -import ApprovalModeOverlay from "../approval-mode-overlay.js"; -import DiffOverlay from "../diff-overlay.js"; -import HelpOverlay from "../help-overlay.js"; -import HistoryOverlay from "../history-overlay.js"; -import ModelOverlay from "../model-overlay.js"; -import SessionsOverlay from "../sessions-overlay.js"; -import chalk from "chalk"; -import fs from "fs/promises"; -import { Box, Text } from "ink"; -import { spawn } from "node:child_process"; -import React, { useEffect, useMemo, useRef, useState } from "react"; -import { inspect } from "util"; - -export type OverlayModeType = - | "none" - | "history" - | "sessions" - | "model" - | "approval" - | "help" - | "diff"; - -type Props = { - config: AppConfig; - prompt?: string; - imagePaths?: Array; - approvalPolicy: ApprovalPolicy; - additionalWritableRoots: ReadonlyArray; - fullStdout: boolean; -}; - -const colorsByPolicy: Record = { - "suggest": undefined, - "auto-edit": "greenBright", - "full-auto": "green", -}; - -/** - * Generates an explanation for a shell command using the OpenAI API. - * - * @param command The command to explain - * @param model The model to use for generating the explanation - * @param flexMode Whether to use the flex-mode service tier - * @param config The configuration object - * @returns A human-readable explanation of what the command does - */ -async function generateCommandExplanation( - command: Array, - model: string, - flexMode: boolean, - config: AppConfig, -): Promise { - try { - // Create a temporary OpenAI client - const oai = createOpenAIClient(config); - - // Format the command for display - const commandForDisplay = formatCommandForDisplay(command); - - // Create a prompt that asks for an explanation with a more detailed system prompt - const response = await oai.chat.completions.create({ - model, - ...(flexMode ? { service_tier: "flex" } : {}), - messages: [ - { - role: "system", - content: - "You are an expert in shell commands and terminal operations. Your task is to provide detailed, accurate explanations of shell commands that users are considering executing. Break down each part of the command, explain what it does, identify any potential risks or side effects, and explain why someone might want to run it. Be specific about what files or systems will be affected. If the command could potentially be harmful, make sure to clearly highlight those risks.", - }, - { - role: "user", - content: `Please explain this shell command in detail: \`${commandForDisplay}\`\n\nProvide a structured explanation that includes:\n1. A brief overview of what the command does\n2. A breakdown of each part of the command (flags, arguments, etc.)\n3. What files, directories, or systems will be affected\n4. Any potential risks or side effects\n5. Why someone might want to run this command\n\nBe specific and technical - this explanation will help the user decide whether to approve or reject the command.`, - }, - ], - }); - - // Extract the explanation from the response - const explanation = - response.choices[0]?.message.content || "Unable to generate explanation."; - return explanation; - } catch (error) { - log(`Error generating command explanation: ${error}`); - - let errorMessage = "Unable to generate explanation due to an error."; - if (error instanceof Error) { - errorMessage = `Unable to generate explanation: ${error.message}`; - - // If it's an API error, check for more specific information - if ("status" in error && typeof error.status === "number") { - // Handle API-specific errors - if (error.status === 401) { - errorMessage = - "Unable to generate explanation: API key is invalid or expired."; - } else if (error.status === 429) { - errorMessage = - "Unable to generate explanation: Rate limit exceeded. Please try again later."; - } else if (error.status >= 500) { - errorMessage = - "Unable to generate explanation: OpenAI service is currently unavailable. Please try again later."; - } - } - } - - return errorMessage; - } -} - -export default function TerminalChat({ - config, - prompt: _initialPrompt, - imagePaths: _initialImagePaths, - approvalPolicy: initialApprovalPolicy, - additionalWritableRoots, - fullStdout, -}: Props): React.ReactElement { - const notify = Boolean(config.notify); - const [model, setModel] = useState(config.model); - const [provider, setProvider] = useState(config.provider || "openai"); - const [lastResponseId, setLastResponseId] = useState(null); - const [items, setItems] = useState>([]); - const [loading, setLoading] = useState(false); - const [approvalPolicy, setApprovalPolicy] = useState( - initialApprovalPolicy, - ); - const [thinkingSeconds, setThinkingSeconds] = useState(0); - - const handleCompact = async () => { - setLoading(true); - try { - const summary = await generateCompactSummary( - items, - model, - Boolean(config.flexMode), - config, - ); - setItems([ - { - id: `compact-${Date.now()}`, - type: "message", - role: "assistant", - content: [{ type: "output_text", text: summary }], - } as ResponseItem, - ]); - } catch (err) { - setItems((prev) => [ - ...prev, - { - id: `compact-error-${Date.now()}`, - type: "message", - role: "system", - content: [ - { type: "input_text", text: `Failed to compact context: ${err}` }, - ], - } as ResponseItem, - ]); - } finally { - setLoading(false); - } - }; - - const { - requestConfirmation, - confirmationPrompt, - explanation, - submitConfirmation, - } = useConfirmation(); - const [overlayMode, setOverlayMode] = useState("none"); - const [viewRollout, setViewRollout] = useState(null); - - // Store the diff text when opening the diff overlay so the view isn’t - // recomputed on every re‑render while it is open. - // diffText is passed down to the DiffOverlay component. The setter is - // currently unused but retained for potential future updates. Prefix with - // an underscore so eslint ignores the unused variable. - const [diffText, _setDiffText] = useState(""); - - const [initialPrompt, setInitialPrompt] = useState(_initialPrompt); - const [initialImagePaths, setInitialImagePaths] = - useState(_initialImagePaths); - - const PWD = React.useMemo(() => shortCwd(), []); - - // Keep a single AgentLoop instance alive across renders; - // recreate only when model/instructions/approvalPolicy change. - const agentRef = React.useRef(); - const [, forceUpdate] = React.useReducer((c) => c + 1, 0); // trigger re‑render - - // ──────────────────────────────────────────────────────────────── - // DEBUG: log every render w/ key bits of state - // ──────────────────────────────────────────────────────────────── - log( - `render - agent? ${Boolean(agentRef.current)} loading=${loading} items=${ - items.length - }`, - ); - - useEffect(() => { - // Skip recreating the agent if awaiting a decision on a pending confirmation. - if (confirmationPrompt != null) { - log("skip AgentLoop recreation due to pending confirmationPrompt"); - return; - } - - log("creating NEW AgentLoop"); - log( - `model=${model} provider=${provider} instructions=${Boolean( - config.instructions, - )} approvalPolicy=${approvalPolicy}`, - ); - - // Tear down any existing loop before creating a new one. - agentRef.current?.terminate(); - - const sessionId = crypto.randomUUID(); - agentRef.current = new AgentLoop({ - model, - provider, - config, - instructions: config.instructions, - approvalPolicy, - disableResponseStorage: config.disableResponseStorage, - additionalWritableRoots, - onLastResponseId: setLastResponseId, - onItem: (item) => { - log(`onItem: ${JSON.stringify(item)}`); - setItems((prev) => { - const updated = uniqueById([...prev, item as ResponseItem]); - saveRollout(sessionId, updated); - return updated; - }); - }, - onLoading: setLoading, - getCommandConfirmation: async ( - command: Array, - applyPatch: ApplyPatchCommand | undefined, - ): Promise => { - log(`getCommandConfirmation: ${command}`); - const commandForDisplay = formatCommandForDisplay(command); - - // First request for confirmation - let { decision: review, customDenyMessage } = await requestConfirmation( - , - ); - - // If the user wants an explanation, generate one and ask again. - if (review === ReviewDecision.EXPLAIN) { - log(`Generating explanation for command: ${commandForDisplay}`); - const explanation = await generateCommandExplanation( - command, - model, - Boolean(config.flexMode), - config, - ); - log(`Generated explanation: ${explanation}`); - - // Ask for confirmation again, but with the explanation. - const confirmResult = await requestConfirmation( - , - ); - - // Update the decision based on the second confirmation. - review = confirmResult.decision; - customDenyMessage = confirmResult.customDenyMessage; - - // Return the final decision with the explanation. - return { review, customDenyMessage, applyPatch, explanation }; - } - - return { review, customDenyMessage, applyPatch }; - }, - }); - - // Force a render so JSX below can "see" the freshly created agent. - forceUpdate(); - - log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); - - return () => { - log("terminating AgentLoop"); - agentRef.current?.terminate(); - agentRef.current = undefined; - forceUpdate(); // re‑render after teardown too - }; - // We intentionally omit 'approvalPolicy' and 'confirmationPrompt' from the deps - // so switching modes or showing confirmation dialogs doesn’t tear down the loop. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [model, provider, config, requestConfirmation, additionalWritableRoots]); - - // Whenever loading starts/stops, reset or start a timer — but pause the - // timer while a confirmation overlay is displayed so we don't trigger a - // re‑render every second during apply_patch reviews. - useEffect(() => { - let handle: ReturnType | null = null; - // Only tick the "thinking…" timer when the agent is actually processing - // a request *and* the user is not being asked to review a command. - if (loading && confirmationPrompt == null) { - setThinkingSeconds(0); - handle = setInterval(() => { - setThinkingSeconds((s) => s + 1); - }, 1000); - } else { - if (handle) { - clearInterval(handle); - } - setThinkingSeconds(0); - } - return () => { - if (handle) { - clearInterval(handle); - } - }; - }, [loading, confirmationPrompt]); - - // Notify desktop with a preview when an assistant response arrives. - const prevLoadingRef = useRef(false); - useEffect(() => { - // Only notify when notifications are enabled. - if (!notify) { - prevLoadingRef.current = loading; - return; - } - - if ( - prevLoadingRef.current && - !loading && - confirmationPrompt == null && - items.length > 0 - ) { - if (process.platform === "darwin") { - // find the last assistant message - const assistantMessages = items.filter( - (i) => i.type === "message" && i.role === "assistant", - ); - const last = assistantMessages[assistantMessages.length - 1]; - if (last) { - const text = last.content - .map((c) => { - if (c.type === "output_text") { - return c.text; - } - return ""; - }) - .join("") - .trim(); - const preview = text.replace(/\n/g, " ").slice(0, 100); - const safePreview = preview.replace(/"/g, '\\"'); - const title = "Codex CLI"; - const cwd = PWD; - spawn("osascript", [ - "-e", - `display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"`, - ]); - } - } - } - prevLoadingRef.current = loading; - }, [notify, loading, confirmationPrompt, items, PWD]); - - // Let's also track whenever the ref becomes available. - const agent = agentRef.current; - useEffect(() => { - log(`agentRef.current is now ${Boolean(agent)}`); - }, [agent]); - - // --------------------------------------------------------------------- - // Dynamic layout constraints – keep total rendered rows <= terminal rows - // --------------------------------------------------------------------- - - const { rows: terminalRows } = useTerminalSize(); - - useEffect(() => { - const processInitialInputItems = async () => { - if ( - (!initialPrompt || initialPrompt.trim() === "") && - (!initialImagePaths || initialImagePaths.length === 0) - ) { - return; - } - const inputItems = [ - await createInputItem(initialPrompt || "", initialImagePaths || []), - ]; - // Clear them to prevent subsequent runs. - setInitialPrompt(""); - setInitialImagePaths([]); - agent?.run(inputItems); - }; - processInitialInputItems(); - }, [agent, initialPrompt, initialImagePaths]); - - // ──────────────────────────────────────────────────────────────── - // In-app warning if CLI --model isn't in fetched list - // ──────────────────────────────────────────────────────────────── - useEffect(() => { - (async () => { - const available = await getAvailableModels(provider); - if (model && available.length > 0 && !available.includes(model)) { - setItems((prev) => [ - ...prev, - { - id: `unknown-model-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Warning: model "${model}" is not in the list of available models for provider "${provider}".`, - }, - ], - }, - ]); - } - })(); - // run once on mount - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Just render every item in order, no grouping/collapse. - const lastMessageBatch = items.map((item) => ({ item })); - const groupCounts: Record = {}; - const userMsgCount = items.filter( - (i) => i.type === "message" && i.role === "user", - ).length; - - const contextLeftPercent = useMemo( - () => calculateContextPercentRemaining(items, model), - [items, model], - ); - - if (viewRollout) { - return ( - - ); - } - - return ( - - - {agent ? ( - - ) : ( - - Initializing agent… - - )} - {overlayMode === "none" && agent && ( - - submitConfirmation({ - decision, - customDenyMessage, - }) - } - contextLeftPercent={contextLeftPercent} - openOverlay={() => setOverlayMode("history")} - openModelOverlay={() => setOverlayMode("model")} - openApprovalOverlay={() => setOverlayMode("approval")} - openHelpOverlay={() => setOverlayMode("help")} - openSessionsOverlay={() => setOverlayMode("sessions")} - openDiffOverlay={() => { - const { isGitRepo, diff } = getGitDiff(); - let text: string; - if (isGitRepo) { - text = diff; - } else { - text = "`/diff` — _not inside a git repository_"; - } - setItems((prev) => [ - ...prev, - { - id: `diff-${Date.now()}`, - type: "message", - role: "system", - content: [{ type: "input_text", text }], - }, - ]); - // Ensure no overlay is shown. - setOverlayMode("none"); - }} - onCompact={handleCompact} - active={overlayMode === "none"} - interruptAgent={() => { - if (!agent) { - return; - } - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - agent.cancel(); - setLoading(false); - - // Add a system message to indicate the interruption - setItems((prev) => [ - ...prev, - { - id: `interrupt-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: "⏹️ Execution interrupted by user. You can continue typing.", - }, - ], - }, - ]); - }} - submitInput={(inputs) => { - agent.run(inputs, lastResponseId || ""); - return {}; - }} - items={items} - thinkingSeconds={thinkingSeconds} - /> - )} - {overlayMode === "history" && ( - setOverlayMode("none")} /> - )} - {overlayMode === "sessions" && ( - { - try { - const txt = await fs.readFile(p, "utf-8"); - const data = JSON.parse(txt) as AppRollout; - setViewRollout(data); - setOverlayMode("none"); - } catch { - setOverlayMode("none"); - } - }} - onResume={(p) => { - setOverlayMode("none"); - setInitialPrompt(`Resume this session: ${p}`); - }} - onExit={() => setOverlayMode("none")} - /> - )} - {overlayMode === "model" && ( - { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - if (!agent) { - log("TerminalChat: agent is not ready yet"); - } - agent?.cancel(); - setLoading(false); - - if (!allModels?.includes(newModel)) { - // eslint-disable-next-line no-console - console.error( - chalk.bold.red( - `Model "${chalk.yellow( - newModel, - )}" is not available for provider "${chalk.yellow( - provider, - )}".`, - ), - ); - return; - } - - setModel(newModel); - setLastResponseId((prev) => - prev && newModel !== model ? null : prev, - ); - - // Save model to config - saveConfig({ - ...config, - model: newModel, - provider: provider, - }); - - setItems((prev) => [ - ...prev, - { - id: `switch-model-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Switched model to ${newModel}`, - }, - ], - }, - ]); - - setOverlayMode("none"); - }} - onSelectProvider={(newProvider) => { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - if (!agent) { - log("TerminalChat: agent is not ready yet"); - } - agent?.cancel(); - setLoading(false); - - // Select default model for the new provider. - const defaultModel = model; - - // Save provider to config. - const updatedConfig = { - ...config, - provider: newProvider, - model: defaultModel, - }; - saveConfig(updatedConfig); - - setProvider(newProvider); - setModel(defaultModel); - setLastResponseId((prev) => - prev && newProvider !== provider ? null : prev, - ); - - setItems((prev) => [ - ...prev, - { - id: `switch-provider-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Switched provider to ${newProvider} with model ${defaultModel}`, - }, - ], - }, - ]); - - // Don't close the overlay so user can select a model for the new provider - // setOverlayMode("none"); - }} - onExit={() => setOverlayMode("none")} - /> - )} - - {overlayMode === "approval" && ( - { - // Update approval policy without cancelling an in-progress session. - if (newMode === approvalPolicy) { - return; - } - - setApprovalPolicy(newMode as ApprovalPolicy); - if (agentRef.current) { - ( - agentRef.current as unknown as { - approvalPolicy: ApprovalPolicy; - } - ).approvalPolicy = newMode as ApprovalPolicy; - } - setItems((prev) => [ - ...prev, - { - id: `switch-approval-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Switched approval mode to ${newMode}`, - }, - ], - }, - ]); - - setOverlayMode("none"); - }} - onExit={() => setOverlayMode("none")} - /> - )} - - {overlayMode === "help" && ( - setOverlayMode("none")} /> - )} - - {overlayMode === "diff" && ( - setOverlayMode("none")} - /> - )} - - - ); -} diff --git a/codex-cli/src/components/chat/terminal-header.tsx b/codex-cli/src/components/chat/terminal-header.tsx deleted file mode 100644 index 9ba16e6fde..0000000000 --- a/codex-cli/src/components/chat/terminal-header.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import type { AgentLoop } from "../../utils/agent/agent-loop.js"; - -import { Box, Text } from "ink"; -import path from "node:path"; -import React from "react"; - -export interface TerminalHeaderProps { - terminalRows: number; - version: string; - PWD: string; - model: string; - provider?: string; - approvalPolicy: string; - colorsByPolicy: Record; - agent?: AgentLoop; - initialImagePaths?: Array; - flexModeEnabled?: boolean; -} - -const TerminalHeader: React.FC = ({ - terminalRows, - version, - PWD, - model, - provider = "openai", - approvalPolicy, - colorsByPolicy, - agent, - initialImagePaths, - flexModeEnabled = false, -}) => { - return ( - <> - {terminalRows < 10 ? ( - // Compact header for small terminal windows - - ● Codex v{version} - {PWD} - {model} ({provider}) -{" "} - {approvalPolicy} - {flexModeEnabled ? " - flex-mode" : ""} - - ) : ( - <> - - - ● OpenAI Codex{" "} - - (research preview) v{version} - - - - - - localhost session:{" "} - - {agent?.sessionId ?? ""} - - - - ↳ workdir: {PWD} - - - ↳ model: {model} - - - ↳ provider:{" "} - {provider} - - - ↳ approval:{" "} - - {approvalPolicy} - - - {flexModeEnabled && ( - - ↳ flex-mode:{" "} - enabled - - )} - {initialImagePaths?.map((img, idx) => ( - - ↳ image:{" "} - {path.basename(img)} - - ))} - - - )} - - ); -}; - -export default TerminalHeader; diff --git a/codex-cli/src/components/chat/terminal-message-history.tsx b/codex-cli/src/components/chat/terminal-message-history.tsx deleted file mode 100644 index 5036f0813d..0000000000 --- a/codex-cli/src/components/chat/terminal-message-history.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import type { OverlayModeType } from "./terminal-chat.js"; -import type { TerminalHeaderProps } from "./terminal-header.js"; -import type { GroupedResponseItem } from "./use-message-grouping.js"; -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; -import type { FileOpenerScheme } from "src/utils/config.js"; - -import TerminalChatResponseItem from "./terminal-chat-response-item.js"; -import TerminalHeader from "./terminal-header.js"; -import { Box, Static } from "ink"; -import React, { useMemo } from "react"; - -// A batch entry can either be a standalone response item or a grouped set of -// items (e.g. auto‑approved tool‑call batches) that should be rendered -// together. -type BatchEntry = { item?: ResponseItem; group?: GroupedResponseItem }; -type TerminalMessageHistoryProps = { - batch: Array; - groupCounts: Record; - items: Array; - userMsgCount: number; - confirmationPrompt: React.ReactNode; - loading: boolean; - thinkingSeconds: number; - headerProps: TerminalHeaderProps; - fullStdout: boolean; - setOverlayMode: React.Dispatch>; - fileOpener: FileOpenerScheme | undefined; -}; - -const TerminalMessageHistory: React.FC = ({ - batch, - headerProps, - // `loading` and `thinkingSeconds` handled by input component now. - loading: _loading, - thinkingSeconds: _thinkingSeconds, - fullStdout, - setOverlayMode, - fileOpener, -}) => { - // Flatten batch entries to response items. - const messages = useMemo(() => batch.map(({ item }) => item!), [batch]); - - return ( - - {/* The dedicated thinking indicator in the input area now displays the - elapsed time, so we no longer render a separate counter here. */} - - {(item, index) => { - if (item === "header") { - return ; - } - - // After the guard above, item is a ResponseItem - const message = item as ResponseItem; - // Suppress empty reasoning updates (i.e. items with an empty summary). - const msg = message as unknown as { summary?: Array }; - if (msg.summary?.length === 0) { - return null; - } - return ( - - - - ); - }} - - - ); -}; - -export default React.memo(TerminalMessageHistory); diff --git a/codex-cli/src/components/chat/use-message-grouping.ts b/codex-cli/src/components/chat/use-message-grouping.ts deleted file mode 100644 index 1e526821d0..0000000000 --- a/codex-cli/src/components/chat/use-message-grouping.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; - -/** - * Represents a grouped sequence of response items (e.g., function call batches). - */ -export type GroupedResponseItem = { - label: string; - items: Array; -}; diff --git a/codex-cli/src/components/diff-overlay.tsx b/codex-cli/src/components/diff-overlay.tsx deleted file mode 100644 index 8de85b87d5..0000000000 --- a/codex-cli/src/components/diff-overlay.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Box, Text, useInput } from "ink"; -import React, { useState } from "react"; - -/** - * Simple scrollable view for displaying a diff. - * The component is intentionally lightweight and mirrors the UX of - * HistoryOverlay: Up/Down or j/k to scroll, PgUp/PgDn for paging and Esc to - * close. The caller is responsible for computing the diff text. - */ -export default function DiffOverlay({ - diffText, - onExit, -}: { - diffText: string; - onExit: () => void; -}): JSX.Element { - const lines = diffText.length > 0 ? diffText.split("\n") : ["(no changes)"]; - - const [cursor, setCursor] = useState(0); - - // Determine how many rows we can display – similar to HistoryOverlay. - const rows = process.stdout.rows || 24; - const headerRows = 2; - const footerRows = 1; - const maxVisible = Math.max(4, rows - headerRows - footerRows); - - useInput((input, key) => { - if (key.escape || input === "q") { - onExit(); - return; - } - - if (key.downArrow || input === "j") { - setCursor((c) => Math.min(lines.length - 1, c + 1)); - } else if (key.upArrow || input === "k") { - setCursor((c) => Math.max(0, c - 1)); - } else if (key.pageDown) { - setCursor((c) => Math.min(lines.length - 1, c + maxVisible)); - } else if (key.pageUp) { - setCursor((c) => Math.max(0, c - maxVisible)); - } else if (input === "g") { - setCursor(0); - } else if (input === "G") { - setCursor(lines.length - 1); - } - }); - - const firstVisible = Math.min( - Math.max(0, cursor - Math.floor(maxVisible / 2)), - Math.max(0, lines.length - maxVisible), - ); - const visible = lines.slice(firstVisible, firstVisible + maxVisible); - - // Very small helper to colorize diff lines in a basic way. - function renderLine(line: string, idx: number): JSX.Element { - let color: "green" | "red" | "cyan" | undefined = undefined; - if (line.startsWith("+")) { - color = "green"; - } else if (line.startsWith("-")) { - color = "red"; - } else if (line.startsWith("@@") || line.startsWith("diff --git")) { - color = "cyan"; - } - return ( - - {line === "" ? " " : line} - - ); - } - - return ( - - - Working tree diff ({lines.length} lines) - - - - {visible.map((line, idx) => { - return renderLine(line, firstVisible + idx); - })} - - - - esc Close ↑↓ Scroll PgUp/PgDn g/G First/Last - - - ); -} diff --git a/codex-cli/src/components/help-overlay.tsx b/codex-cli/src/components/help-overlay.tsx deleted file mode 100644 index d302f7551e..0000000000 --- a/codex-cli/src/components/help-overlay.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Box, Text, useInput } from "ink"; -import React from "react"; - -/** - * An overlay that lists the available slash‑commands and their description. - * The overlay is purely informational and can be dismissed with the Escape - * key. Keeping the implementation extremely small avoids adding any new - * dependencies or complex state handling. - */ -export default function HelpOverlay({ - onExit, -}: { - onExit: () => void; -}): JSX.Element { - useInput((input, key) => { - if (key.escape || input === "q") { - onExit(); - } - }); - - return ( - - - Available commands - - - - - Slash‑commands - - - /help – show this help overlay - - - /model – switch the LLM model in‑session - - - /approval – switch auto‑approval mode - - - /history – show command & file history - for this session - - - /clear – clear screen & context - - - /clearhistory – clear command history - - - /bug – generate a prefilled GitHub issue URL - with session log - - - /diff – view working tree git diff - - - /compact – condense context into a summary - - - - - Keyboard shortcuts - - - - Enter – send message - - - Ctrl+J – insert newline - - {/* Re-enable once we re-enable new input */} - {/* - - Ctrl+X/Ctrl+E -  – open external editor ($EDITOR) - - */} - - Up/Down – scroll prompt history - - - - Esc(✕2) - {" "} - – interrupt current action - - - Ctrl+C – quit Codex - - - - - esc or q to close - - - ); -} diff --git a/codex-cli/src/components/history-overlay.tsx b/codex-cli/src/components/history-overlay.tsx deleted file mode 100644 index f6ea8464e1..0000000000 --- a/codex-cli/src/components/history-overlay.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; - -import { Box, Text, useInput } from "ink"; -import React, { useMemo, useState } from "react"; - -type Props = { - items: Array; - onExit: () => void; -}; - -type Mode = "commands" | "files"; - -export default function HistoryOverlay({ items, onExit }: Props): JSX.Element { - const [mode, setMode] = useState("commands"); - const [cursor, setCursor] = useState(0); - - const { commands, files } = useMemo( - () => formatHistoryForDisplay(items), - [items], - ); - - const list = mode === "commands" ? commands : files; - - useInput((input, key) => { - if (key.escape) { - onExit(); - return; - } - - if (input === "c") { - setMode("commands"); - setCursor(0); - return; - } - if (input === "f") { - setMode("files"); - setCursor(0); - return; - } - - if (key.downArrow || input === "j") { - setCursor((c) => Math.min(list.length - 1, c + 1)); - } else if (key.upArrow || input === "k") { - setCursor((c) => Math.max(0, c - 1)); - } else if (key.pageDown) { - setCursor((c) => Math.min(list.length - 1, c + 10)); - } else if (key.pageUp) { - setCursor((c) => Math.max(0, c - 10)); - } else if (input === "g") { - setCursor(0); - } else if (input === "G") { - setCursor(list.length - 1); - } - }); - - const rows = process.stdout.rows || 24; - const headerRows = 2; - const footerRows = 1; - const maxVisible = Math.max(4, rows - headerRows - footerRows); - - const firstVisible = Math.min( - Math.max(0, cursor - Math.floor(maxVisible / 2)), - Math.max(0, list.length - maxVisible), - ); - const visible = list.slice(firstVisible, firstVisible + maxVisible); - - return ( - - - - {mode === "commands" ? "Commands run" : "Files touched"} ( - {list.length}) - - - - {visible.map((txt, idx) => { - const absIdx = firstVisible + idx; - const selected = absIdx === cursor; - return ( - - {selected ? "› " : " "} - {txt} - - ); - })} - - - - esc Close ↑↓ Scroll PgUp/PgDn g/G First/Last c Commands f Files - - - - ); -} - -function formatHistoryForDisplay(items: Array): { - commands: Array; - files: Array; -} { - const commands: Array = []; - const filesSet = new Set(); - - for (const item of items) { - const userPrompt = processUserMessage(item); - if (userPrompt) { - commands.push(userPrompt); - continue; - } - - // ------------------------------------------------------------------ - // We are interested in tool calls which – for the OpenAI client – are - // represented as `function_call` response items. Skip everything else. - if (item.type !== "function_call") { - continue; - } - - const { name: toolName, arguments: argsString } = item as unknown as { - name: unknown; - arguments: unknown; - }; - - if (typeof argsString !== "string") { - // Malformed – still record the tool name to give users maximal context. - if (typeof toolName === "string" && toolName.length > 0) { - commands.push(toolName); - } - continue; - } - - // Best‑effort attempt to parse the JSON arguments. We never throw on parse - // failure – the history view must be resilient to bad data. - let argsJson: unknown = undefined; - try { - argsJson = JSON.parse(argsString); - } catch { - argsJson = undefined; - } - - // 1) Shell / exec‑like tool calls expose a `cmd` or `command` property - // that is an array of strings. These are rendered as the joined command - // line for familiarity with traditional shells. - const argsObj = argsJson as Record | undefined; - const cmdArray: Array | undefined = Array.isArray(argsObj?.["cmd"]) - ? (argsObj!["cmd"] as Array) - : Array.isArray(argsObj?.["command"]) - ? (argsObj!["command"] as Array) - : undefined; - - if (cmdArray && cmdArray.length > 0) { - commands.push(processCommandArray(cmdArray, filesSet)); - continue; // We processed this as a command; no need to treat as generic tool call. - } - - // 2) Non‑exec tool calls – we fall back to recording the tool name plus a - // short argument representation to give users an idea of what - // happened. - if (typeof toolName === "string" && toolName.length > 0) { - commands.push(processNonExecTool(toolName, argsJson, filesSet)); - } - } - - return { commands, files: Array.from(filesSet) }; -} - -function processUserMessage(item: ResponseItem): string | null { - if ( - item.type === "message" && - (item as unknown as { role?: string }).role === "user" - ) { - // TODO: We're ignoring images/files here. - const parts = - (item as unknown as { content?: Array }).content ?? []; - const texts: Array = []; - if (Array.isArray(parts)) { - for (const part of parts) { - if (part && typeof part === "object" && "text" in part) { - const t = (part as unknown as { text?: string }).text; - if (typeof t === "string" && t.length > 0) { - texts.push(t); - } - } - } - } - - if (texts.length > 0) { - const fullPrompt = texts.join(" "); - // Truncate very long prompts so the history view stays legible. - return fullPrompt.length > 120 - ? `> ${fullPrompt.slice(0, 117)}…` - : `> ${fullPrompt}`; - } - } - return null; -} - -function processCommandArray( - cmdArray: Array, - filesSet: Set, -): string { - const cmd = cmdArray.join(" "); - - // Heuristic for file paths in command args - for (const part of cmdArray) { - if (!part.startsWith("-") && part.includes("/")) { - filesSet.add(part); - } - } - - // Special‑case apply_patch so we can extract the list of modified files - if (cmdArray[0] === "apply_patch" || cmdArray.includes("apply_patch")) { - const patchTextMaybe = cmdArray.find((s) => s.includes("*** Begin Patch")); - if (typeof patchTextMaybe === "string") { - const lines = patchTextMaybe.split("\n"); - for (const line of lines) { - const m = line.match(/^[-+]{3} [ab]\/(.+)$/); - if (m && m[1]) { - filesSet.add(m[1]); - } - } - } - } - - return cmd; -} - -function processNonExecTool( - toolName: string, - argsJson: unknown, - filesSet: Set, -): string { - let summary = toolName; - - if (argsJson && typeof argsJson === "object") { - // Extract a few common argument keys to make the summary more useful - // without being overly verbose. - const interestingKeys = ["path", "file", "filepath", "filename", "pattern"]; - for (const key of interestingKeys) { - const val = (argsJson as Record)[key]; - if (typeof val === "string") { - summary += ` ${val}`; - if (val.includes("/")) { - filesSet.add(val); - } - break; - } - } - } - - return summary; -} diff --git a/codex-cli/src/components/model-overlay.tsx b/codex-cli/src/components/model-overlay.tsx deleted file mode 100644 index 86a7e5850d..0000000000 --- a/codex-cli/src/components/model-overlay.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import TypeaheadOverlay from "./typeahead-overlay.js"; -import { - getAvailableModels, - RECOMMENDED_MODELS as _RECOMMENDED_MODELS, -} from "../utils/model-utils.js"; -import { Box, Text, useInput } from "ink"; -import React, { useEffect, useState } from "react"; - -/** - * Props for . - * - * When `hasLastResponse` is true the user has already received at least one - * assistant response in the current session which means switching models is no - * longer supported – the overlay should therefore show an error and only allow - * the user to close it. - */ -type Props = { - currentModel: string; - currentProvider?: string; - hasLastResponse: boolean; - providers?: Record; - onSelect: (allModels: Array, model: string) => void; - onSelectProvider?: (provider: string) => void; - onExit: () => void; -}; - -export default function ModelOverlay({ - currentModel, - providers = {}, - currentProvider = "openai", - hasLastResponse, - onSelect, - onSelectProvider, - onExit, -}: Props): JSX.Element { - const [items, setItems] = useState>( - [], - ); - const [providerItems, _setProviderItems] = useState< - Array<{ label: string; value: string }> - >(Object.values(providers).map((p) => ({ label: p.name, value: p.name }))); - const [mode, setMode] = useState<"model" | "provider">("model"); - const [isLoading, setIsLoading] = useState(true); - - // This effect will run when the provider changes to update the model list - useEffect(() => { - setIsLoading(true); - (async () => { - try { - const models = await getAvailableModels(currentProvider); - // Convert the models to the format needed by TypeaheadOverlay - setItems( - models.map((m) => ({ - label: m, - value: m, - })), - ); - } catch (error) { - // Silently handle errors - remove console.error - // console.error("Error loading models:", error); - } finally { - setIsLoading(false); - } - })(); - }, [currentProvider]); - - // --------------------------------------------------------------------------- - // If the conversation already contains a response we cannot change the model - // anymore because the backend requires a consistent model across the entire - // run. In that scenario we replace the regular typeahead picker with a - // simple message instructing the user to start a new chat. The only - // available action is to dismiss the overlay (Esc or Enter). - // --------------------------------------------------------------------------- - - // Register input handling for switching between model and provider selection - useInput((_input, key) => { - if (hasLastResponse && (key.escape || key.return)) { - onExit(); - } else if (!hasLastResponse) { - if (key.tab) { - setMode(mode === "model" ? "provider" : "model"); - } - } - }); - - if (hasLastResponse) { - return ( - - - - Unable to switch model - - - - - You can only pick a model before the assistant sends its first - response. To use a different model please start a new chat. - - - - press esc or enter to close - - - ); - } - - if (mode === "provider") { - return ( - - - Current provider:{" "} - {currentProvider} - - press tab to switch to model selection - - } - initialItems={providerItems} - currentValue={currentProvider} - onSelect={(provider) => { - if (onSelectProvider) { - onSelectProvider(provider); - // Immediately switch to model selection so user can pick a model for the new provider - setMode("model"); - } - }} - onExit={onExit} - /> - ); - } - - return ( - - - Current model: {currentModel} - - - Current provider: {currentProvider} - - {isLoading && Loading models...} - press tab to switch to provider selection - - } - initialItems={items} - currentValue={currentModel} - onSelect={(selectedModel) => - onSelect( - items?.map((m) => m.value), - selectedModel, - ) - } - onExit={onExit} - /> - ); -} diff --git a/codex-cli/src/components/onboarding/onboarding-approval-mode.tsx b/codex-cli/src/components/onboarding/onboarding-approval-mode.tsx deleted file mode 100644 index f095c6c04b..0000000000 --- a/codex-cli/src/components/onboarding/onboarding-approval-mode.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-expect-error select.js is JavaScript and has no types -import { Select } from "../vendor/ink-select/select"; -import { Box, Text } from "ink"; -import React from "react"; -import { AutoApprovalMode } from "src/utils/auto-approval-mode"; - -// TODO: figure out why `cli-spinners` fails on Node v20.9.0 -// which is why we have to do this in the first place - -export function OnboardingApprovalMode(): React.ReactElement { - return ( - - Choose what you want to have to approve: - [+ cached] + let mut input_line_spans: Vec> = vec![ + " • Input: ".into(), + usage.non_cached_input().to_string().into(), ]; + if let Some(cached) = usage.cached_input_tokens { + if cached > 0 { + input_line_spans.push(format!(" (+ {cached} cached)").into()); + } + } + lines.push(Line::from(input_line_spans)); + // Output: + lines.push(Line::from(vec![ + " • Output: ".into(), + usage.output_tokens.to_string().into(), + ])); + // Total: + lines.push(Line::from(vec![ + " • Total: ".into(), + usage.blended_total().to_string().into(), + ])); + + lines.push(Line::from("")); + HistoryCell::StatusOutput { + view: TextBlock::new(lines), + } + } + + pub(crate) fn new_prompts_output() -> Self { + let lines: Vec> = vec![ + Line::from("/prompts".magenta()), + Line::from(""), + Line::from(" 1. Explain this codebase"), + Line::from(" 2. Summarize recent commits"), + Line::from(" 3. Implement {feature}"), + Line::from(" 4. Find and fix a bug in @filename"), + Line::from(" 5. Write tests for @filename"), + Line::from(" 6. Improve documentation in @filename"), + Line::from(""), + ]; + HistoryCell::PromptsOutput { + view: TextBlock::new(lines), + } + } + + pub(crate) fn new_error_event(message: String) -> Self { + let lines: Vec> = + vec![vec!["🖐 ".red().bold(), message.into()].into(), "".into()]; HistoryCell::ErrorEvent { view: TextBlock::new(lines), } } - /// Render a user‑friendly plan update with colourful status icons and a - /// simple progress indicator so users can follow along. + /// Render a user‑friendly plan update styled like a checkbox todo list. pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> Self { let UpdatePlanArgs { explanation, plan } = update; let mut lines: Vec> = Vec::new(); + // Header with progress summary + let total = plan.len(); + let completed = plan + .iter() + .filter(|p| matches!(p.status, StepStatus::Completed)) + .count(); - // Title - lines.push(Line::from("plan".magenta().bold())); + let width: usize = 10; + let filled = if total > 0 { + (completed * width + total / 2) / total + } else { + 0 + }; + let empty = width.saturating_sub(filled); - if !plan.is_empty() { - // Progress bar – show completed/total with a visual bar - let total = plan.len(); - let completed = plan - .iter() - .filter(|p| matches!(p.status, StepStatus::Completed)) - .count(); - let width: usize = 20; - let filled = (completed * width + total / 2) / total; - let empty = width.saturating_sub(filled); - let mut bar_spans: Vec = Vec::new(); - if filled > 0 { - bar_spans.push(Span::styled( - "█".repeat(filled), - Style::default().fg(Color::Green), - )); - } - if empty > 0 { - bar_spans.push(Span::styled( - "░".repeat(empty), - Style::default().fg(Color::Gray), - )); - } - let progress_prefix = Span::raw("progress ["); - let progress_suffix = Span::raw("] "); - let fraction = Span::raw(format!("{completed}/{total}")); - let mut progress_line_spans = vec![progress_prefix]; - progress_line_spans.extend(bar_spans); - progress_line_spans.push(progress_suffix); - progress_line_spans.push(fraction); - lines.push(Line::from(progress_line_spans)); + let mut header: Vec = Vec::new(); + header.push(Span::raw("📋")); + header.push(Span::styled( + " Updated", + Style::default().add_modifier(Modifier::BOLD).magenta(), + )); + header.push(Span::raw(" to do list [")); + if filled > 0 { + header.push(Span::styled( + "█".repeat(filled), + Style::default().fg(Color::Green), + )); } + if empty > 0 { + header.push(Span::styled( + "░".repeat(empty), + Style::default().fg(Color::Gray), + )); + } + header.push(Span::raw("] ")); + header.push(Span::raw(format!("{completed}/{total}"))); + lines.push(Line::from(header)); // Optional explanation/note from the model if let Some(expl) = explanation.and_then(|s| { @@ -539,22 +713,48 @@ impl HistoryCell { } } - // Steps (1‑based numbering) with fun, readable status icons + // Steps styled as checkbox items if plan.is_empty() { lines.push(Line::from("(no steps provided)".gray().italic())); } else { for (idx, PlanItemArg { step, status }) in plan.into_iter().enumerate() { - let num = idx + 1; - let icon_span: Span = match status { - StepStatus::Completed => Span::from("✓").fg(Color::Green), - StepStatus::InProgress => Span::from("▶").fg(Color::Yellow).bold(), - StepStatus::Pending => Span::from("○").fg(Color::Gray), + let (box_span, text_span) = match status { + StepStatus::Completed => ( + Span::styled("✔", Style::default().fg(Color::Green)), + Span::styled( + step, + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::CROSSED_OUT | Modifier::DIM), + ), + ), + StepStatus::InProgress => ( + Span::raw("□"), + Span::styled( + step, + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD), + ), + ), + StepStatus::Pending => ( + Span::raw("□"), + Span::styled( + step, + Style::default().fg(Color::Gray).add_modifier(Modifier::DIM), + ), + ), + }; + let prefix = if idx == 0 { + Span::raw(" ⎿ ") + } else { + Span::raw(" ") }; lines.push(Line::from(vec![ - format!("{num:>2}. [").into(), - icon_span, - "] ".into(), - step.into(), + prefix, + box_span, + Span::raw(" "), + text_span, ])); } } @@ -577,50 +777,26 @@ impl HistoryCell { PatchEventType::ApprovalRequest => "proposed patch", PatchEventType::ApplyBegin { auto_approved: true, - } => "applying patch", + } => "✏️ Applying patch", PatchEventType::ApplyBegin { auto_approved: false, } => { - let lines = vec![Line::from("patch applied".magenta().bold())]; + let lines: Vec> = vec![ + Line::from("✏️ Applying patch".magenta().bold()), + Line::from(""), + ]; return Self::PendingPatch { view: TextBlock::new(lines), }; } }; - let summary_lines = create_diff_summary(changes); + let summary_lines = create_diff_summary(title, changes); let mut lines: Vec> = Vec::new(); - // Header similar to the command formatter so patches are visually - // distinct while still fitting the overall colour scheme. - lines.push(Line::from(title.magenta().bold())); - for line in summary_lines { - if line.starts_with('+') { - lines.push(line.green().into()); - } else if line.starts_with('-') { - lines.push(line.red().into()); - } else if let Some(space_idx) = line.find(' ') { - let kind_owned = line[..space_idx].to_string(); - let rest_owned = line[space_idx + 1..].to_string(); - - let style_for = |fg: Color| Style::default().fg(fg).add_modifier(Modifier::BOLD); - - let styled_kind = match kind_owned.as_str() { - "A" => RtSpan::styled(kind_owned.clone(), style_for(Color::Green)), - "D" => RtSpan::styled(kind_owned.clone(), style_for(Color::Red)), - "M" => RtSpan::styled(kind_owned.clone(), style_for(Color::Yellow)), - "R" | "C" => RtSpan::styled(kind_owned.clone(), style_for(Color::Cyan)), - _ => RtSpan::raw(kind_owned.clone()), - }; - - let styled_line = - RtLine::from(vec![styled_kind, RtSpan::raw(" "), RtSpan::raw(rest_owned)]); - lines.push(styled_line); - } else { - lines.push(Line::from(line)); - } + lines.push(line); } lines.push(Line::from("")); @@ -629,38 +805,175 @@ impl HistoryCell { view: TextBlock::new(lines), } } + + pub(crate) fn new_patch_apply_failure(stderr: String) -> Self { + let mut lines: Vec> = Vec::new(); + + // Failure title + lines.push(Line::from("✘ Failed to apply patch".magenta().bold())); + + if !stderr.trim().is_empty() { + let mut iter = stderr.lines(); + for (i, raw) in iter.by_ref().take(TOOL_CALL_MAX_LINES).enumerate() { + let prefix = if i == 0 { " ⎿ " } else { " " }; + let s = format!("{prefix}{raw}"); + lines.push(ansi_escape_line(&s).dim()); + } + let remaining = iter.count(); + if remaining > 0 { + lines.push(Line::from("")); + lines.push(Line::from(format!("... +{remaining} lines")).dim()); + } + } + + lines.push(Line::from("")); + + HistoryCell::PatchApplyResult { + view: TextBlock::new(lines), + } + } } -fn create_diff_summary(changes: HashMap) -> Vec { - // Build a concise, human‑readable summary list similar to the - // `git status` short format so the user can reason about the - // patch without scrolling. - let mut summaries: Vec = Vec::new(); +impl WidgetRef for &HistoryCell { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + Paragraph::new(Text::from(self.plain_lines())) + .wrap(Wrap { trim: false }) + .render(area, buf); + } +} + +fn create_diff_summary(title: &str, changes: HashMap) -> Vec> { + let mut files: Vec = Vec::new(); + + // Count additions/deletions from a unified diff body + let count_from_unified = |diff: &str| -> (usize, usize) { + if let Ok(patch) = diffy::Patch::from_str(diff) { + let mut adds = 0usize; + let mut dels = 0usize; + for hunk in patch.hunks() { + for line in hunk.lines() { + match line { + diffy::Line::Insert(_) => adds += 1, + diffy::Line::Delete(_) => dels += 1, + _ => {} + } + } + } + (adds, dels) + } else { + let mut adds = 0usize; + let mut dels = 0usize; + for l in diff.lines() { + if l.starts_with("+++") || l.starts_with("---") || l.starts_with("@@") { + continue; + } + match l.as_bytes().first() { + Some(b'+') => adds += 1, + Some(b'-') => dels += 1, + _ => {} + } + } + (adds, dels) + } + }; + for (path, change) in &changes { use codex_core::protocol::FileChange::*; match change { Add { content } => { let added = content.lines().count(); - summaries.push(format!("A {} (+{added})", path.display())); + files.push(FileSummary { + display_path: path.display().to_string(), + added, + removed: 0, + }); } Delete => { - summaries.push(format!("D {}", path.display())); + let removed = std::fs::read_to_string(path) + .ok() + .map(|s| s.lines().count()) + .unwrap_or(0); + files.push(FileSummary { + display_path: path.display().to_string(), + added: 0, + removed, + }); } Update { unified_diff, move_path, } => { - if let Some(new_path) = move_path { - summaries.push(format!("R {} → {}", path.display(), new_path.display(),)); + let (added, removed) = count_from_unified(unified_diff); + let display_path = if let Some(new_path) = move_path { + format!("{} → {}", path.display(), new_path.display()) } else { - summaries.push(format!("M {}", path.display(),)); - } - summaries.extend(unified_diff.lines().map(|s| s.to_string())); + path.display().to_string() + }; + files.push(FileSummary { + display_path, + added, + removed, + }); } } } - summaries + let file_count = files.len(); + let total_added: usize = files.iter().map(|f| f.added).sum(); + let total_removed: usize = files.iter().map(|f| f.removed).sum(); + let noun = if file_count == 1 { "file" } else { "files" }; + + let mut out: Vec> = Vec::new(); + + // Header + let mut header_spans: Vec> = Vec::new(); + header_spans.push(RtSpan::styled( + title.to_owned(), + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + )); + header_spans.push(RtSpan::raw(" to ")); + header_spans.push(RtSpan::raw(format!("{file_count} {noun} "))); + header_spans.push(RtSpan::raw("(")); + header_spans.push(RtSpan::styled( + format!("+{total_added}"), + Style::default().fg(Color::Green), + )); + header_spans.push(RtSpan::raw(" ")); + header_spans.push(RtSpan::styled( + format!("-{total_removed}"), + Style::default().fg(Color::Red), + )); + header_spans.push(RtSpan::raw(")")); + out.push(RtLine::from(header_spans)); + + // Dimmed per-file lines with prefix + for (idx, f) in files.iter().enumerate() { + let mut spans: Vec> = Vec::new(); + spans.push(RtSpan::raw(f.display_path.clone())); + spans.push(RtSpan::raw(" (")); + spans.push(RtSpan::styled( + format!("+{}", f.added), + Style::default().fg(Color::Green), + )); + spans.push(RtSpan::raw(" ")); + spans.push(RtSpan::styled( + format!("-{}", f.removed), + Style::default().fg(Color::Red), + )); + spans.push(RtSpan::raw(")")); + + let mut line = RtLine::from(spans); + let prefix = if idx == 0 { " ⎿ " } else { " " }; + line.spans.insert(0, prefix.into()); + line.spans.iter_mut().for_each(|span| { + span.style = span.style.add_modifier(Modifier::DIM); + }); + out.push(line); + } + + out } fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> { diff --git a/codex-rs/tui/src/insert_history.rs b/codex-rs/tui/src/insert_history.rs index 87d88b7f03..5c316637b1 100644 --- a/codex-rs/tui/src/insert_history.rs +++ b/codex-rs/tui/src/insert_history.rs @@ -14,7 +14,6 @@ use crossterm::style::SetBackgroundColor; use crossterm::style::SetColors; use crossterm::style::SetForegroundColor; use ratatui::layout::Size; -use ratatui::prelude::Backend; use ratatui::style::Color; use ratatui::style::Modifier; use ratatui::text::Line; @@ -22,6 +21,20 @@ use ratatui::text::Span; /// Insert `lines` above the viewport. pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec) { + let mut out = std::io::stdout(); + insert_history_lines_to_writer(terminal, &mut out, lines); +} + +/// Like `insert_history_lines`, but writes ANSI to the provided writer. This +/// is intended for testing where a capture buffer is used instead of stdout. +pub fn insert_history_lines_to_writer( + terminal: &mut crate::custom_terminal::Terminal, + writer: &mut W, + lines: Vec, +) where + B: ratatui::backend::Backend, + W: Write, +{ let screen_size = terminal.backend().size().unwrap_or(Size::new(0, 0)); let cursor_pos = terminal.get_cursor_position().ok(); @@ -32,10 +45,22 @@ pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec) { // If the viewport is not at the bottom of the screen, scroll it down to make room. // Don't scroll it past the bottom of the screen. let scroll_amount = wrapped_lines.min(screen_size.height - area.bottom()); - terminal - .backend_mut() - .scroll_region_down(area.top()..screen_size.height, scroll_amount) - .ok(); + + // Emit ANSI to scroll the lower region (from the top of the viewport to the bottom + // of the screen) downward by `scroll_amount` lines. We do this by: + // 1) Limiting the scroll region to [area.top()+1 .. screen_height] (1-based bounds) + // 2) Placing the cursor at the top margin of that region + // 3) Emitting Reverse Index (RI, ESC M) `scroll_amount` times + // 4) Resetting the scroll region back to full screen + let top_1based = area.top() + 1; // Convert 0-based row to 1-based for DECSTBM + queue!(writer, SetScrollRegion(top_1based..screen_size.height)).ok(); + queue!(writer, MoveTo(0, area.top())).ok(); + for _ in 0..scroll_amount { + // Reverse Index (RI): ESC M + queue!(writer, Print("\x1bM")).ok(); + } + queue!(writer, ResetScrollRegion).ok(); + let cursor_top = area.top().saturating_sub(1); area.y += scroll_amount; terminal.set_viewport_area(area); @@ -59,23 +84,23 @@ pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec) { // ││ ││ // │╰────────────────────────────╯│ // └──────────────────────────────┘ - queue!(std::io::stdout(), SetScrollRegion(1..area.top())).ok(); + queue!(writer, SetScrollRegion(1..area.top())).ok(); // NB: we are using MoveTo instead of set_cursor_position here to avoid messing with the // terminal's last_known_cursor_position, which hopefully will still be accurate after we // fetch/restore the cursor position. insert_history_lines should be cursor-position-neutral :) - queue!(std::io::stdout(), MoveTo(0, cursor_top)).ok(); + queue!(writer, MoveTo(0, cursor_top)).ok(); for line in lines { - queue!(std::io::stdout(), Print("\r\n")).ok(); - write_spans(&mut std::io::stdout(), line.iter()).ok(); + queue!(writer, Print("\r\n")).ok(); + write_spans(writer, line.iter()).ok(); } - queue!(std::io::stdout(), ResetScrollRegion).ok(); + queue!(writer, ResetScrollRegion).ok(); // Restore the cursor position to where it was before we started. if let Some(cursor_pos) = cursor_pos { - queue!(std::io::stdout(), MoveTo(cursor_pos.x, cursor_pos.y)).ok(); + queue!(writer, MoveTo(cursor_pos.x, cursor_pos.y)).ok(); } } @@ -88,19 +113,25 @@ fn wrapped_line_count(lines: &[Line], width: u16) -> u16 { } fn line_height(line: &Line, width: u16) -> u16 { - use unicode_width::UnicodeWidthStr; - // get the total display width of the line, accounting for double-width chars - let total_width = line + // Use the same visible-width slicing semantics as the live row builder so + // our pre-scroll estimation matches how rows will actually wrap. + let w = width.max(1) as usize; + let mut rows = 0u16; + let mut remaining = line .spans .iter() - .map(|span| span.content.width()) - .sum::(); - // divide by width to get the number of lines, rounding up - if width == 0 { - 1 - } else { - (total_width as u16).div_ceil(width).max(1) + .map(|s| s.content.as_ref()) + .collect::>() + .join(""); + while !remaining.is_empty() { + let (_prefix, suffix, taken) = crate::live_wrap::take_prefix_by_width(&remaining, w); + rows = rows.saturating_add(1); + if taken >= remaining.len() { + break; + } + remaining = suffix.to_string(); } + rows.max(1) } #[derive(Debug, Clone, PartialEq, Eq)] @@ -283,4 +314,12 @@ mod tests { String::from_utf8(expected).unwrap() ); } + + #[test] + fn line_height_counts_double_width_emoji() { + let line = Line::from("😀😀😀"); // each emoji ~ width 2 + assert_eq!(line_height(&line, 4), 2); + assert_eq!(line_height(&line, 2), 3); + assert_eq!(line_height(&line, 6), 1); + } } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 6101e5cefc..ac33c91a5c 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -3,15 +3,19 @@ // alternate‑screen mode starts; that file opts‑out locally via `allow`. #![deny(clippy::print_stdout, clippy::print_stderr)] use app::App; +use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config::ConfigToml; +use codex_core::config::find_codex_home; +use codex_core::config::load_config_as_toml_with_cli_overrides; use codex_core::config_types::SandboxMode; use codex_core::protocol::AskForApproval; -use codex_core::util::is_inside_git_repo; -use codex_login::load_auth; +use codex_core::protocol::SandboxPolicy; +use codex_login::CodexAuth; +use codex_ollama::DEFAULT_OSS_MODEL; use log_layer::TuiLogLayer; use std::fs::OpenOptions; -use std::io::Write; use std::path::PathBuf; use tracing::error; use tracing_appender::non_blocking; @@ -26,15 +30,18 @@ mod chatwidget; mod citation_regex; mod cli; mod clipboard_paste; -mod custom_terminal; +mod colors; +pub mod custom_terminal; mod exec_command; mod file_search; mod get_git_diff; -mod git_warning_screen; mod history_cell; -mod insert_history; +pub mod insert_history; +pub mod live_wrap; mod log_layer; mod markdown; +pub mod onboarding; +mod shimmer; mod slash_command; mod status_indicator_widget; mod text_block; @@ -42,6 +49,11 @@ mod text_formatting; mod tui; mod user_approval_widget; +#[cfg(not(debug_assertions))] +mod updates; +#[cfg(not(debug_assertions))] +use color_eyre::owo_colors::OwoColorize; + pub use cli::Cli; pub async fn run_main( @@ -65,31 +77,55 @@ pub async fn run_main( ) }; - let config = { + // When using `--oss`, let the bootstrapper pick the model (defaulting to + // gpt-oss:20b) and ensure it is present locally. Also, force the built‑in + // `oss` model provider. + let model = if let Some(model) = &cli.model { + Some(model.clone()) + } else if cli.oss { + Some(DEFAULT_OSS_MODEL.to_owned()) + } else { + None // No model specified, will use the default. + }; + + let model_provider_override = if cli.oss { + Some(BUILT_IN_OSS_MODEL_PROVIDER_ID.to_owned()) + } else { + None + }; + + // canonicalize the cwd + let cwd = cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)); + + let overrides = ConfigOverrides { + model, + approval_policy, + sandbox_mode, + cwd, + model_provider: model_provider_override, + config_profile: cli.config_profile.clone(), + codex_linux_sandbox_exe, + base_instructions: None, + include_plan_tool: Some(true), + disable_response_storage: cli.oss.then_some(true), + show_raw_agent_reasoning: cli.oss.then_some(true), + }; + + // Parse `-c` overrides from the CLI. + let cli_kv_overrides = match cli.config_overrides.parse_overrides() { + Ok(v) => v, + #[allow(clippy::print_stderr)] + Err(e) => { + eprintln!("Error parsing -c overrides: {e}"); + std::process::exit(1); + } + }; + + let mut config = { // Load configuration and support CLI overrides. - let overrides = ConfigOverrides { - model: cli.model.clone(), - approval_policy, - sandbox_mode, - cwd: cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)), - model_provider: None, - config_profile: cli.config_profile.clone(), - codex_linux_sandbox_exe, - base_instructions: None, - include_plan_tool: Some(true), - }; - // Parse `-c` overrides from the CLI. - let cli_kv_overrides = match cli.config_overrides.parse_overrides() { - Ok(v) => v, - #[allow(clippy::print_stderr)] - Err(e) => { - eprintln!("Error parsing -c overrides: {e}"); - std::process::exit(1); - } - }; #[allow(clippy::print_stderr)] - match Config::load_with_cli_overrides(cli_kv_overrides, overrides) { + match Config::load_with_cli_overrides(cli_kv_overrides.clone(), overrides) { Ok(config) => config, Err(err) => { eprintln!("Error loading configuration: {err}"); @@ -98,6 +134,34 @@ pub async fn run_main( } }; + // we load config.toml here to determine project state. + #[allow(clippy::print_stderr)] + let config_toml = { + let codex_home = match find_codex_home() { + Ok(codex_home) => codex_home, + Err(err) => { + eprintln!("Error finding codex home: {err}"); + std::process::exit(1); + } + }; + + match load_config_as_toml_with_cli_overrides(&codex_home, cli_kv_overrides) { + Ok(config_toml) => config_toml, + Err(err) => { + eprintln!("Error loading config.toml: {err}"); + std::process::exit(1); + } + } + }; + + let should_show_trust_screen = determine_repo_trust_state( + &mut config, + &config_toml, + approval_policy, + sandbox_mode, + cli.config_profile.clone(), + )?; + let log_dir = codex_core::config::log_dir(&config)?; std::fs::create_dir_all(&log_dir)?; // Open (or create) your log file, appending to it. @@ -131,6 +195,12 @@ pub async fn run_main( .with_target(false) .with_filter(env_filter()); + if cli.oss { + codex_ollama::ensure_oss_ready(&config) + .await + .map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?; + } + // Channel that carries formatted log lines to the UI. let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel::(); let tui_layer = TuiLogLayer::new(log_tx.clone(), 120).with_filter(env_filter()); @@ -140,38 +210,46 @@ pub async fn run_main( .with(tui_layer) .try_init(); - let show_login_screen = should_show_login_screen(&config); - if show_login_screen { - std::io::stdout() - .write_all(b"No API key detected.\nLogin with your ChatGPT account? [Yn] ")?; - std::io::stdout().flush()?; - let mut input = String::new(); - std::io::stdin().read_line(&mut input)?; - let trimmed = input.trim(); - if !(trimmed.is_empty() || trimmed.eq_ignore_ascii_case("y")) { - std::process::exit(1); - } - // Spawn a task to run the login command. - // Block until the login command is finished. - codex_login::login_with_chatgpt(&config.codex_home, false).await?; + #[allow(clippy::print_stderr)] + #[cfg(not(debug_assertions))] + if let Some(latest_version) = updates::get_upgrade_version(&config) { + let current_version = env!("CARGO_PKG_VERSION"); + let exe = std::env::current_exe()?; + let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some(); - std::io::stdout().write_all(b"Login successful.\n")?; + eprintln!( + "{} {current_version} -> {latest_version}.", + "✨⬆️ Update available!".bold().cyan() + ); + + if managed_by_npm { + let npm_cmd = "npm install -g @openai/codex@latest"; + eprintln!("Run {} to update.", npm_cmd.cyan().on_black()); + } else if cfg!(target_os = "macos") + && (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local")) + { + let brew_cmd = "brew upgrade codex"; + eprintln!("Run {} to update.", brew_cmd.cyan().on_black()); + } else { + eprintln!( + "See {} for the latest releases and installation options.", + "https://github.com/openai/codex/releases/latest" + .cyan() + .on_black() + ); + } + + eprintln!(""); } - // Determine whether we need to display the "not a git repo" warning - // modal. The flag is shown when the current working directory is *not* - // inside a Git repository **and** the user did *not* pass the - // `--allow-no-git-exec` flag. - let show_git_warning = !cli.skip_git_repo_check && !is_inside_git_repo(&config); - - run_ratatui_app(cli, config, show_git_warning, log_rx) + run_ratatui_app(cli, config, should_show_trust_screen, log_rx) .map_err(|err| std::io::Error::other(err.to_string())) } fn run_ratatui_app( cli: Cli, config: Config, - show_git_warning: bool, + should_show_trust_screen: bool, mut log_rx: tokio::sync::mpsc::UnboundedReceiver, ) -> color_eyre::Result { color_eyre::install()?; @@ -189,7 +267,7 @@ fn run_ratatui_app( terminal.clear()?; let Cli { prompt, images, .. } = cli; - let mut app = App::new(config.clone(), prompt, show_git_warning, images); + let mut app = App::new(config.clone(), prompt, images, should_show_trust_screen); // Bridge log receiver into the AppEvent channel so latest log lines update the UI. { @@ -223,11 +301,11 @@ fn restore() { #[allow(clippy::unwrap_used)] fn should_show_login_screen(config: &Config) -> bool { - if config.model_provider.requires_auth { + if config.model_provider.requires_openai_auth { // Reading the OpenAI API key is an async operation because it may need // to refresh the token. Block on it. let codex_home = config.codex_home.clone(); - match load_auth(&codex_home, true) { + match CodexAuth::from_codex_home(&codex_home) { Ok(Some(_)) => false, Ok(None) => true, Err(err) => { @@ -239,3 +317,39 @@ fn should_show_login_screen(config: &Config) -> bool { false } } + +/// Determine if user has configured a sandbox / approval policy, +/// or if the current cwd project is trusted, and updates the config +/// accordingly. +fn determine_repo_trust_state( + config: &mut Config, + config_toml: &ConfigToml, + approval_policy_overide: Option, + sandbox_mode_override: Option, + config_profile_override: Option, +) -> std::io::Result { + let config_profile = config_toml.get_config_profile(config_profile_override)?; + + if approval_policy_overide.is_some() || sandbox_mode_override.is_some() { + // if the user has overridden either approval policy or sandbox mode, + // skip the trust flow + Ok(false) + } else if config_profile.approval_policy.is_some() { + // if the user has specified settings in a config profile, skip the trust flow + // todo: profile sandbox mode? + Ok(false) + } else if config_toml.approval_policy.is_some() || config_toml.sandbox_mode.is_some() { + // if the user has specified either approval policy or sandbox mode in config.toml + // skip the trust flow + Ok(false) + } else if config_toml.is_cwd_trusted(&config.cwd) { + // if the current cwd project is trusted and no config has been set + // skip the trust flow and set the approval policy and sandbox mode + config.approval_policy = AskForApproval::OnRequest; + config.sandbox_policy = SandboxPolicy::new_workspace_write_policy(); + Ok(false) + } else { + // if none of the above conditions are met, show the trust screen + Ok(true) + } +} diff --git a/codex-rs/tui/src/live_wrap.rs b/codex-rs/tui/src/live_wrap.rs new file mode 100644 index 0000000000..e78710dc6c --- /dev/null +++ b/codex-rs/tui/src/live_wrap.rs @@ -0,0 +1,290 @@ +use unicode_width::UnicodeWidthChar; +use unicode_width::UnicodeWidthStr; + +/// A single visual row produced by RowBuilder. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Row { + pub text: String, + /// True if this row ends with an explicit line break (as opposed to a hard wrap). + pub explicit_break: bool, +} + +impl Row { + pub fn width(&self) -> usize { + self.text.width() + } +} + +/// Incrementally wraps input text into visual rows of at most `width` cells. +/// +/// Step 1: plain-text only. ANSI-carry and styled spans will be added later. +pub struct RowBuilder { + target_width: usize, + /// Buffer for the current logical line (until a '\n' is seen). + current_line: String, + /// Output rows built so far for the current logical line and previous ones. + rows: Vec, +} + +impl RowBuilder { + pub fn new(target_width: usize) -> Self { + Self { + target_width: target_width.max(1), + current_line: String::new(), + rows: Vec::new(), + } + } + + pub fn width(&self) -> usize { + self.target_width + } + + pub fn set_width(&mut self, width: usize) { + self.target_width = width.max(1); + // Rewrap everything we have (simple approach for Step 1). + let mut all = String::new(); + for row in self.rows.drain(..) { + all.push_str(&row.text); + if row.explicit_break { + all.push('\n'); + } + } + all.push_str(&self.current_line); + self.current_line.clear(); + self.push_fragment(&all); + } + + /// Push an input fragment. May contain newlines. + pub fn push_fragment(&mut self, fragment: &str) { + if fragment.is_empty() { + return; + } + let mut start = 0usize; + for (i, ch) in fragment.char_indices() { + if ch == '\n' { + // Flush anything pending before the newline. + if start < i { + self.current_line.push_str(&fragment[start..i]); + } + self.flush_current_line(true); + start = i + ch.len_utf8(); + } + } + if start < fragment.len() { + self.current_line.push_str(&fragment[start..]); + self.wrap_current_line(); + } + } + + /// Mark the end of the current logical line (equivalent to pushing a '\n'). + pub fn end_line(&mut self) { + self.flush_current_line(true); + } + + /// Drain and return all produced rows. + pub fn drain_rows(&mut self) -> Vec { + std::mem::take(&mut self.rows) + } + + /// Return a snapshot of produced rows (non-draining). + pub fn rows(&self) -> &[Row] { + &self.rows + } + + /// Rows suitable for display, including the current partial line if any. + pub fn display_rows(&self) -> Vec { + let mut out = self.rows.clone(); + if !self.current_line.is_empty() { + out.push(Row { + text: self.current_line.clone(), + explicit_break: false, + }); + } + out + } + + /// Drain the oldest rows that exceed `max_keep` display rows (including the + /// current partial line, if any). Returns the drained rows in order. + pub fn drain_commit_ready(&mut self, max_keep: usize) -> Vec { + let display_count = self.rows.len() + if self.current_line.is_empty() { 0 } else { 1 }; + if display_count <= max_keep { + return Vec::new(); + } + let to_commit = display_count - max_keep; + let commit_count = to_commit.min(self.rows.len()); + let mut drained = Vec::with_capacity(commit_count); + for _ in 0..commit_count { + drained.push(self.rows.remove(0)); + } + drained + } + + fn flush_current_line(&mut self, explicit_break: bool) { + // Wrap any remaining content in the current line and then finalize with explicit_break. + self.wrap_current_line(); + // If the current line ended exactly on a width boundary and is non-empty, represent + // the explicit break as an empty explicit row so that fragmentation invariance holds. + if explicit_break { + if self.current_line.is_empty() { + // We ended on a boundary previously; add an empty explicit row. + self.rows.push(Row { + text: String::new(), + explicit_break: true, + }); + } else { + // There is leftover content that did not wrap yet; push it now with the explicit flag. + let mut s = String::new(); + std::mem::swap(&mut s, &mut self.current_line); + self.rows.push(Row { + text: s, + explicit_break: true, + }); + } + } + // Reset current line buffer for next logical line. + self.current_line.clear(); + } + + fn wrap_current_line(&mut self) { + // While the current_line exceeds width, cut a prefix. + loop { + if self.current_line.is_empty() { + break; + } + let (prefix, suffix, taken) = + take_prefix_by_width(&self.current_line, self.target_width); + if taken == 0 { + // Avoid infinite loop on pathological inputs; take one scalar and continue. + if let Some((i, ch)) = self.current_line.char_indices().next() { + let len = i + ch.len_utf8(); + let p = self.current_line[..len].to_string(); + self.rows.push(Row { + text: p, + explicit_break: false, + }); + self.current_line = self.current_line[len..].to_string(); + continue; + } + break; + } + if suffix.is_empty() { + // Fits entirely; keep in buffer (do not push yet) so we can append more later. + break; + } else { + // Emit wrapped prefix as a non-explicit row and continue with the remainder. + self.rows.push(Row { + text: prefix, + explicit_break: false, + }); + self.current_line = suffix.to_string(); + } + } + } +} + +/// Take a prefix of `text` whose visible width is at most `max_cols`. +/// Returns (prefix, suffix, prefix_width). +pub fn take_prefix_by_width(text: &str, max_cols: usize) -> (String, &str, usize) { + if max_cols == 0 || text.is_empty() { + return (String::new(), text, 0); + } + let mut cols = 0usize; + let mut end_idx = 0usize; + for (i, ch) in text.char_indices() { + let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0); + if cols.saturating_add(ch_width) > max_cols { + break; + } + cols += ch_width; + end_idx = i + ch.len_utf8(); + if cols == max_cols { + break; + } + } + let prefix = text[..end_idx].to_string(); + let suffix = &text[end_idx..]; + (prefix, suffix, cols) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn rows_do_not_exceed_width_ascii() { + let mut rb = RowBuilder::new(10); + rb.push_fragment("hello whirl this is a test"); + let rows = rb.rows().to_vec(); + assert_eq!( + rows, + vec![ + Row { + text: "hello whir".to_string(), + explicit_break: false + }, + Row { + text: "l this is ".to_string(), + explicit_break: false + } + ] + ); + } + + #[test] + fn rows_do_not_exceed_width_emoji_cjk() { + // 😀 is width 2; 你/好 are width 2. + let mut rb = RowBuilder::new(6); + rb.push_fragment("😀😀 你好"); + let rows = rb.rows().to_vec(); + // At width 6, we expect the first row to fit exactly two emojis and a space + // (2 + 2 + 1 = 5) plus one more column for the first CJK char (2 would overflow), + // so only the two emojis and the space fit; the rest remains buffered. + assert_eq!( + rows, + vec![Row { + text: "😀😀 ".to_string(), + explicit_break: false + }] + ); + } + + #[test] + fn fragmentation_invariance_long_token() { + let s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // 26 chars + let mut rb_all = RowBuilder::new(7); + rb_all.push_fragment(s); + let all_rows = rb_all.rows().to_vec(); + + let mut rb_chunks = RowBuilder::new(7); + for i in (0..s.len()).step_by(3) { + let end = (i + 3).min(s.len()); + rb_chunks.push_fragment(&s[i..end]); + } + let chunk_rows = rb_chunks.rows().to_vec(); + + assert_eq!(all_rows, chunk_rows); + } + + #[test] + fn newline_splits_rows() { + let mut rb = RowBuilder::new(10); + rb.push_fragment("hello\nworld"); + let rows = rb.display_rows(); + assert!(rows.iter().any(|r| r.explicit_break)); + assert_eq!(rows[0].text, "hello"); + // Second row should begin with 'world' + assert!(rows.iter().any(|r| r.text.starts_with("world"))); + } + + #[test] + fn rewrap_on_width_change() { + let mut rb = RowBuilder::new(10); + rb.push_fragment("abcdefghijK"); + assert!(!rb.rows().is_empty()); + rb.set_width(5); + for r in rb.rows() { + assert!(r.width() <= 5); + } + } +} diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs index 209febf035..2dbd797dd4 100644 --- a/codex-rs/tui/src/main.rs +++ b/codex-rs/tui/src/main.rs @@ -22,7 +22,9 @@ fn main() -> anyhow::Result<()> { .raw_overrides .splice(0..0, top_cli.config_overrides.raw_overrides); let usage = run_main(inner, codex_linux_sandbox_exe).await?; - println!("{}", codex_core::protocol::FinalOutput::from(usage)); + if !usage.is_zero() { + println!("{}", codex_core::protocol::FinalOutput::from(usage)); + } Ok(()) }) } diff --git a/codex-rs/tui/src/markdown.rs b/codex-rs/tui/src/markdown.rs index ab20138298..910a6869ec 100644 --- a/codex-rs/tui/src/markdown.rs +++ b/codex-rs/tui/src/markdown.rs @@ -1,3 +1,4 @@ +use crate::citation_regex::CITATION_REGEX; use codex_core::config::Config; use codex_core::config_types::UriBasedFileOpener; use ratatui::text::Line; @@ -5,8 +6,7 @@ use ratatui::text::Span; use std::borrow::Cow; use std::path::Path; -use crate::citation_regex::CITATION_REGEX; - +#[allow(dead_code)] pub(crate) fn append_markdown( markdown_source: &str, lines: &mut Vec>, @@ -15,6 +15,7 @@ pub(crate) fn append_markdown( append_markdown_with_opener_and_cwd(markdown_source, lines, config.file_opener, &config.cwd); } +#[allow(dead_code)] fn append_markdown_with_opener_and_cwd( markdown_source: &str, lines: &mut Vec>, @@ -60,6 +61,7 @@ fn append_markdown_with_opener_and_cwd( /// ```text /// ://file: /// ``` +#[allow(dead_code)] fn rewrite_file_citations<'a>( src: &'a str, file_opener: UriBasedFileOpener, diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs new file mode 100644 index 0000000000..6276552849 --- /dev/null +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -0,0 +1,403 @@ +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::prelude::Widget; +use ratatui::style::Color; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::text::Line; +use ratatui::text::Span; +use ratatui::widgets::Paragraph; +use ratatui::widgets::WidgetRef; +use ratatui::widgets::Wrap; + +use codex_login::AuthMode; + +use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; +use crate::colors::LIGHT_BLUE; +use crate::colors::SUCCESS_GREEN; +use crate::onboarding::onboarding_screen::KeyboardHandler; +use crate::onboarding::onboarding_screen::StepStateProvider; +use crate::shimmer::FrameTicker; +use crate::shimmer::shimmer_spans; +use std::path::PathBuf; + +use super::onboarding_screen::StepState; +// no additional imports + +#[derive(Debug)] +pub(crate) enum SignInState { + PickMode, + ChatGptContinueInBrowser(ContinueInBrowserState), + ChatGptSuccessMessage, + ChatGptSuccess, + EnvVarMissing, + EnvVarFound, +} + +#[derive(Debug)] +/// Used to manage the lifecycle of SpawnedLogin and FrameTicker and ensure they get cleaned up. +pub(crate) struct ContinueInBrowserState { + login_child: Option, + _frame_ticker: Option, +} +impl Drop for ContinueInBrowserState { + fn drop(&mut self) { + if let Some(child) = &self.login_child { + if let Ok(mut locked) = child.child.lock() { + // Best-effort terminate and reap the child to avoid zombies. + let _ = locked.kill(); + let _ = locked.wait(); + } + } + } +} + +impl KeyboardHandler for AuthModeWidget { + fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Up | KeyCode::Char('k') => { + self.highlighted_mode = AuthMode::ChatGPT; + } + KeyCode::Down | KeyCode::Char('j') => { + self.highlighted_mode = AuthMode::ApiKey; + } + KeyCode::Char('1') => { + self.start_chatgpt_login(); + } + KeyCode::Char('2') => self.verify_api_key(), + KeyCode::Enter => match self.sign_in_state { + SignInState::PickMode => match self.highlighted_mode { + AuthMode::ChatGPT => self.start_chatgpt_login(), + AuthMode::ApiKey => self.verify_api_key(), + }, + SignInState::EnvVarMissing => self.sign_in_state = SignInState::PickMode, + SignInState::ChatGptSuccessMessage => { + self.sign_in_state = SignInState::ChatGptSuccess + } + _ => {} + }, + KeyCode::Esc => { + if matches!(self.sign_in_state, SignInState::ChatGptContinueInBrowser(_)) { + self.sign_in_state = SignInState::PickMode; + } + } + _ => {} + } + } +} + +#[derive(Debug)] +pub(crate) struct AuthModeWidget { + pub event_tx: AppEventSender, + pub highlighted_mode: AuthMode, + pub error: Option, + pub sign_in_state: SignInState, + pub codex_home: PathBuf, +} + +impl AuthModeWidget { + fn render_pick_mode(&self, area: Rect, buf: &mut Buffer) { + let mut lines: Vec = vec![ + Line::from(vec![ + Span::raw("> "), + Span::styled( + "Sign in with ChatGPT to use Codex as part of your paid plan", + Style::default().add_modifier(Modifier::BOLD), + ), + ]), + Line::from(vec![ + Span::raw(" "), + Span::styled( + "or connect an API key for usage-based billing", + Style::default().add_modifier(Modifier::BOLD), + ), + ]), + Line::from(""), + ]; + + let create_mode_item = |idx: usize, + selected_mode: AuthMode, + text: &str, + description: &str| + -> Vec> { + let is_selected = self.highlighted_mode == selected_mode; + let caret = if is_selected { ">" } else { " " }; + + let line1 = if is_selected { + Line::from(vec![ + Span::styled( + format!("{} {}. ", caret, idx + 1), + Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM), + ), + Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)), + ]) + } else { + Line::from(format!(" {}. {text}", idx + 1)) + }; + + let line2 = if is_selected { + Line::from(format!(" {description}")) + .style(Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM)) + } else { + Line::from(format!(" {description}")) + .style(Style::default().add_modifier(Modifier::DIM)) + }; + + vec![line1, line2] + }; + + lines.extend(create_mode_item( + 0, + AuthMode::ChatGPT, + "Sign in with ChatGPT", + "Usage included with Plus, Pro, and Team plans", + )); + lines.extend(create_mode_item( + 1, + AuthMode::ApiKey, + "Provide your own API key", + "Pay for what you use", + )); + lines.push(Line::from("")); + lines.push( + Line::from(" Press Enter to continue") + .style(Style::default().add_modifier(Modifier::DIM)), + ); + if let Some(err) = &self.error { + lines.push(Line::from("")); + lines.push(Line::from(Span::styled( + err.as_str(), + Style::default().fg(Color::Red), + ))); + } + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn render_continue_in_browser(&self, area: Rect, buf: &mut Buffer) { + let idx = self.current_frame(); + let mut spans = vec![Span::from("> ")]; + spans.extend(shimmer_spans("Finish signing in via your browser", idx)); + let mut lines = vec![Line::from(spans), Line::from("")]; + + if let SignInState::ChatGptContinueInBrowser(state) = &self.sign_in_state { + if let Some(url) = state + .login_child + .as_ref() + .and_then(|child| child.get_login_url()) + { + lines.push(Line::from(" If the link doesn't open automatically, open the following link to authenticate:")); + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled( + url, + Style::default() + .fg(LIGHT_BLUE) + .add_modifier(Modifier::UNDERLINED), + ), + ])); + lines.push(Line::from("")); + } + } + + lines.push( + Line::from(" Press Esc to cancel").style(Style::default().add_modifier(Modifier::DIM)), + ); + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn render_chatgpt_success_message(&self, area: Rect, buf: &mut Buffer) { + let lines = vec![ + Line::from("✓ Signed in with your ChatGPT account") + .style(Style::default().fg(SUCCESS_GREEN)), + Line::from(""), + Line::from("> Before you start:"), + Line::from(""), + Line::from(" Decide how much autonomy you want to grant Codex"), + Line::from(vec![ + Span::raw(" For more details see the "), + Span::styled( + "\u{1b}]8;;https://github.com/openai/codex\u{7}Codex docs\u{1b}]8;;\u{7}", + Style::default().add_modifier(Modifier::UNDERLINED), + ), + ]) + .style(Style::default().add_modifier(Modifier::DIM)), + Line::from(""), + Line::from(" Codex can make mistakes") + .style(Style::default().fg(Color::White)), + Line::from(" Review the code it writes and commands it runs") + .style(Style::default().add_modifier(Modifier::DIM)), + Line::from(""), + Line::from(" Powered by your ChatGPT account"), + Line::from(vec![ + Span::raw(" Uses your plan's rate limits and "), + Span::styled( + "\u{1b}]8;;https://chatgpt.com/#settings\u{7}training data preferences\u{1b}]8;;\u{7}", + Style::default().add_modifier(Modifier::UNDERLINED), + ), + ]) + .style(Style::default().add_modifier(Modifier::DIM)), + Line::from(""), + Line::from(" Press Enter to continue").style(Style::default().fg(LIGHT_BLUE)), + ]; + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) { + let lines = vec![ + Line::from("✓ Signed in with your ChatGPT account") + .style(Style::default().fg(SUCCESS_GREEN)), + ]; + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn render_env_var_found(&self, area: Rect, buf: &mut Buffer) { + let lines = + vec![Line::from("✓ Using OPENAI_API_KEY").style(Style::default().fg(SUCCESS_GREEN))]; + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn render_env_var_missing(&self, area: Rect, buf: &mut Buffer) { + let lines = vec![ + Line::from( + " To use Codex with the OpenAI API, set OPENAI_API_KEY in your environment", + ) + .style(Style::default().fg(Color::Blue)), + Line::from(""), + Line::from(" Press Enter to return") + .style(Style::default().add_modifier(Modifier::DIM)), + ]; + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn start_chatgpt_login(&mut self) { + self.error = None; + match codex_login::spawn_login_with_chatgpt(&self.codex_home) { + Ok(child) => { + self.spawn_completion_poller(child.clone()); + self.sign_in_state = + SignInState::ChatGptContinueInBrowser(ContinueInBrowserState { + login_child: Some(child), + _frame_ticker: Some(FrameTicker::new(self.event_tx.clone())), + }); + self.event_tx.send(AppEvent::RequestRedraw); + } + Err(e) => { + self.sign_in_state = SignInState::PickMode; + self.error = Some(e.to_string()); + self.event_tx.send(AppEvent::RequestRedraw); + } + } + } + + /// TODO: Read/write from the correct hierarchy config overrides + auth json + OPENAI_API_KEY. + fn verify_api_key(&mut self) { + if std::env::var("OPENAI_API_KEY").is_err() { + self.sign_in_state = SignInState::EnvVarMissing; + } else { + self.sign_in_state = SignInState::EnvVarFound; + } + self.event_tx.send(AppEvent::RequestRedraw); + } + + fn spawn_completion_poller(&self, child: codex_login::SpawnedLogin) { + let child_arc = child.child.clone(); + let stderr_buf = child.stderr.clone(); + let event_tx = self.event_tx.clone(); + std::thread::spawn(move || { + loop { + let done = { + if let Ok(mut locked) = child_arc.lock() { + match locked.try_wait() { + Ok(Some(status)) => Some(status.success()), + Ok(None) => None, + Err(_) => Some(false), + } + } else { + Some(false) + } + }; + if let Some(success) = done { + if success { + event_tx.send(AppEvent::OnboardingAuthComplete(Ok(()))); + } else { + let err = stderr_buf + .lock() + .ok() + .and_then(|b| String::from_utf8(b.clone()).ok()) + .unwrap_or_else(|| "login_with_chatgpt subprocess failed".to_string()); + event_tx.send(AppEvent::OnboardingAuthComplete(Err(err))); + } + break; + } + std::thread::sleep(std::time::Duration::from_millis(250)); + } + }); + } + + fn current_frame(&self) -> usize { + // Derive frame index from wall-clock time to avoid storing animation state. + // 100ms per frame to match the previous ticker cadence. + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0); + (now_ms / 100) as usize + } +} + +impl StepStateProvider for AuthModeWidget { + fn get_step_state(&self) -> StepState { + match &self.sign_in_state { + SignInState::PickMode + | SignInState::EnvVarMissing + | SignInState::ChatGptContinueInBrowser(_) + | SignInState::ChatGptSuccessMessage => StepState::InProgress, + SignInState::ChatGptSuccess | SignInState::EnvVarFound => StepState::Complete, + } + } +} + +impl WidgetRef for AuthModeWidget { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + match self.sign_in_state { + SignInState::PickMode => { + self.render_pick_mode(area, buf); + } + SignInState::ChatGptContinueInBrowser(_) => { + self.render_continue_in_browser(area, buf); + } + SignInState::ChatGptSuccessMessage => { + self.render_chatgpt_success_message(area, buf); + } + SignInState::ChatGptSuccess => { + self.render_chatgpt_success(area, buf); + } + SignInState::EnvVarMissing => { + self.render_env_var_missing(area, buf); + } + SignInState::EnvVarFound => { + self.render_env_var_found(area, buf); + } + } + } +} diff --git a/codex-rs/tui/src/onboarding/continue_to_chat.rs b/codex-rs/tui/src/onboarding/continue_to_chat.rs new file mode 100644 index 0000000000..01e31d900a --- /dev/null +++ b/codex-rs/tui/src/onboarding/continue_to_chat.rs @@ -0,0 +1,34 @@ +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::widgets::WidgetRef; + +use crate::app::ChatWidgetArgs; +use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; +use crate::onboarding::onboarding_screen::StepStateProvider; + +use super::onboarding_screen::StepState; +use std::sync::Arc; +use std::sync::Mutex; + +/// This doesn't render anything explicitly but serves as a signal that we made it to the end and +/// we should continue to the chat. +pub(crate) struct ContinueToChatWidget { + pub event_tx: AppEventSender, + pub chat_widget_args: Arc>, +} + +impl StepStateProvider for ContinueToChatWidget { + fn get_step_state(&self) -> StepState { + StepState::Complete + } +} + +impl WidgetRef for &ContinueToChatWidget { + fn render_ref(&self, _area: Rect, _buf: &mut Buffer) { + if let Ok(args) = self.chat_widget_args.lock() { + self.event_tx + .send(AppEvent::OnboardingComplete(args.clone())); + } + } +} diff --git a/codex-rs/tui/src/onboarding/mod.rs b/codex-rs/tui/src/onboarding/mod.rs new file mode 100644 index 0000000000..c116936851 --- /dev/null +++ b/codex-rs/tui/src/onboarding/mod.rs @@ -0,0 +1,5 @@ +mod auth; +mod continue_to_chat; +pub mod onboarding_screen; +mod trust_directory; +mod welcome; diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs new file mode 100644 index 0000000000..a104f777c2 --- /dev/null +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -0,0 +1,267 @@ +use codex_core::util::is_inside_git_repo; +use crossterm::event::KeyEvent; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::widgets::WidgetRef; + +use codex_login::AuthMode; + +use crate::app::ChatWidgetArgs; +use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; +use crate::onboarding::auth::AuthModeWidget; +use crate::onboarding::auth::SignInState; +use crate::onboarding::continue_to_chat::ContinueToChatWidget; +use crate::onboarding::trust_directory::TrustDirectorySelection; +use crate::onboarding::trust_directory::TrustDirectoryWidget; +use crate::onboarding::welcome::WelcomeWidget; +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::Mutex; + +#[allow(clippy::large_enum_variant)] +enum Step { + Welcome(WelcomeWidget), + Auth(AuthModeWidget), + TrustDirectory(TrustDirectoryWidget), + ContinueToChat(ContinueToChatWidget), +} + +pub(crate) trait KeyboardHandler { + fn handle_key_event(&mut self, key_event: KeyEvent); +} + +pub(crate) enum StepState { + Hidden, + InProgress, + Complete, +} + +pub(crate) trait StepStateProvider { + fn get_step_state(&self) -> StepState; +} + +pub(crate) struct OnboardingScreen { + event_tx: AppEventSender, + steps: Vec, +} + +pub(crate) struct OnboardingScreenArgs { + pub event_tx: AppEventSender, + pub chat_widget_args: ChatWidgetArgs, + pub codex_home: PathBuf, + pub cwd: PathBuf, + pub show_login_screen: bool, + pub show_trust_screen: bool, +} + +impl OnboardingScreen { + pub(crate) fn new(args: OnboardingScreenArgs) -> Self { + let OnboardingScreenArgs { + event_tx, + chat_widget_args, + codex_home, + cwd, + show_login_screen, + show_trust_screen, + } = args; + let mut steps: Vec = vec![Step::Welcome(WelcomeWidget { + is_logged_in: !show_login_screen, + })]; + if show_login_screen { + steps.push(Step::Auth(AuthModeWidget { + event_tx: event_tx.clone(), + highlighted_mode: AuthMode::ChatGPT, + error: None, + sign_in_state: SignInState::PickMode, + codex_home: codex_home.clone(), + })) + } + let is_git_repo = is_inside_git_repo(&cwd); + let highlighted = if is_git_repo { + TrustDirectorySelection::Trust + } else { + // Default to not trusting the directory if it's not a git repo. + TrustDirectorySelection::DontTrust + }; + // Share ChatWidgetArgs between steps so changes in the TrustDirectory step + // are reflected when continuing to chat. + let shared_chat_args = Arc::new(Mutex::new(chat_widget_args)); + if show_trust_screen { + steps.push(Step::TrustDirectory(TrustDirectoryWidget { + cwd, + codex_home, + is_git_repo, + selection: None, + highlighted, + error: None, + chat_widget_args: shared_chat_args.clone(), + })) + } + steps.push(Step::ContinueToChat(ContinueToChatWidget { + event_tx: event_tx.clone(), + chat_widget_args: shared_chat_args, + })); + // TODO: add git warning. + Self { event_tx, steps } + } + + pub(crate) fn on_auth_complete(&mut self, result: Result<(), String>) { + let current_step = self.current_step_mut(); + if let Some(Step::Auth(state)) = current_step { + match result { + Ok(()) => { + state.sign_in_state = SignInState::ChatGptSuccessMessage; + self.event_tx.send(AppEvent::RequestRedraw); + } + Err(e) => { + state.sign_in_state = SignInState::PickMode; + state.error = Some(e); + self.event_tx.send(AppEvent::RequestRedraw); + } + } + } + } + + fn current_steps_mut(&mut self) -> Vec<&mut Step> { + let mut out: Vec<&mut Step> = Vec::new(); + for step in self.steps.iter_mut() { + match step.get_step_state() { + StepState::Hidden => continue, + StepState::Complete => out.push(step), + StepState::InProgress => { + out.push(step); + break; + } + } + } + out + } + + fn current_steps(&self) -> Vec<&Step> { + let mut out: Vec<&Step> = Vec::new(); + for step in self.steps.iter() { + match step.get_step_state() { + StepState::Hidden => continue, + StepState::Complete => out.push(step), + StepState::InProgress => { + out.push(step); + break; + } + } + } + out + } + + fn current_step_mut(&mut self) -> Option<&mut Step> { + self.steps + .iter_mut() + .find(|step| matches!(step.get_step_state(), StepState::InProgress)) + } +} + +impl KeyboardHandler for OnboardingScreen { + fn handle_key_event(&mut self, key_event: KeyEvent) { + if let Some(active_step) = self.current_steps_mut().into_iter().last() { + active_step.handle_key_event(key_event); + } + self.event_tx.send(AppEvent::RequestRedraw); + } +} + +impl WidgetRef for &OnboardingScreen { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + // Render steps top-to-bottom, measuring each step's height dynamically. + let mut y = area.y; + let bottom = area.y.saturating_add(area.height); + let width = area.width; + + // Helper to scan a temporary buffer and return number of used rows. + fn used_rows(tmp: &Buffer, width: u16, height: u16) -> u16 { + if width == 0 || height == 0 { + return 0; + } + let mut last_non_empty: Option = None; + for yy in 0..height { + let mut any = false; + for xx in 0..width { + let sym = tmp[(xx, yy)].symbol(); + if !sym.trim().is_empty() { + any = true; + break; + } + } + if any { + last_non_empty = Some(yy); + } + } + last_non_empty.map(|v| v + 2).unwrap_or(0) + } + + let mut i = 0usize; + let current_steps = self.current_steps(); + + while i < current_steps.len() && y < bottom { + let step = ¤t_steps[i]; + let max_h = bottom.saturating_sub(y); + if max_h == 0 || width == 0 { + break; + } + let scratch_area = Rect::new(0, 0, width, max_h); + let mut scratch = Buffer::empty(scratch_area); + step.render_ref(scratch_area, &mut scratch); + let h = used_rows(&scratch, width, max_h).min(max_h); + if h > 0 { + let target = Rect { + x: area.x, + y, + width, + height: h, + }; + step.render_ref(target, buf); + y = y.saturating_add(h); + } + i += 1; + } + } +} + +impl KeyboardHandler for Step { + fn handle_key_event(&mut self, key_event: KeyEvent) { + match self { + Step::Welcome(_) | Step::ContinueToChat(_) => (), + Step::Auth(widget) => widget.handle_key_event(key_event), + Step::TrustDirectory(widget) => widget.handle_key_event(key_event), + } + } +} + +impl StepStateProvider for Step { + fn get_step_state(&self) -> StepState { + match self { + Step::Welcome(w) => w.get_step_state(), + Step::Auth(w) => w.get_step_state(), + Step::TrustDirectory(w) => w.get_step_state(), + Step::ContinueToChat(w) => w.get_step_state(), + } + } +} + +impl WidgetRef for Step { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + match self { + Step::Welcome(widget) => { + widget.render_ref(area, buf); + } + Step::Auth(widget) => { + widget.render_ref(area, buf); + } + Step::TrustDirectory(widget) => { + widget.render_ref(area, buf); + } + Step::ContinueToChat(widget) => { + widget.render_ref(area, buf); + } + } + } +} diff --git a/codex-rs/tui/src/onboarding/trust_directory.rs b/codex-rs/tui/src/onboarding/trust_directory.rs new file mode 100644 index 0000000000..3be9bac1ac --- /dev/null +++ b/codex-rs/tui/src/onboarding/trust_directory.rs @@ -0,0 +1,179 @@ +use std::path::PathBuf; + +use codex_core::config::set_project_trusted; +use codex_core::protocol::AskForApproval; +use codex_core::protocol::SandboxPolicy; +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::prelude::Widget; +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::Paragraph; +use ratatui::widgets::WidgetRef; +use ratatui::widgets::Wrap; + +use crate::colors::LIGHT_BLUE; + +use crate::onboarding::onboarding_screen::KeyboardHandler; +use crate::onboarding::onboarding_screen::StepStateProvider; + +use super::onboarding_screen::StepState; +use crate::app::ChatWidgetArgs; +use std::sync::Arc; +use std::sync::Mutex; + +pub(crate) struct TrustDirectoryWidget { + pub codex_home: PathBuf, + pub cwd: PathBuf, + pub is_git_repo: bool, + pub selection: Option, + pub highlighted: TrustDirectorySelection, + pub error: Option, + pub chat_widget_args: Arc>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum TrustDirectorySelection { + Trust, + DontTrust, +} + +impl WidgetRef for &TrustDirectoryWidget { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + let mut lines: Vec = vec![ + Line::from(vec![ + Span::raw("> "), + Span::styled( + "You are running Codex in ", + Style::default().add_modifier(Modifier::BOLD), + ), + Span::raw(self.cwd.to_string_lossy().to_string()), + ]), + Line::from(""), + ]; + + if self.is_git_repo { + lines.push(Line::from( + " Since this folder is version controlled, you may wish to allow Codex", + )); + lines.push(Line::from( + " to work in this folder without asking for approval.", + )); + } else { + lines.push(Line::from( + " Since this folder is not version controlled, we recommend requiring", + )); + lines.push(Line::from(" approval of all edits and commands.")); + } + lines.push(Line::from("")); + + let create_option = + |idx: usize, option: TrustDirectorySelection, text: &str| -> Line<'static> { + let is_selected = self.highlighted == option; + if is_selected { + Line::from(vec![ + Span::styled( + format!("> {}. ", idx + 1), + Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM), + ), + Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)), + ]) + } else { + Line::from(format!(" {}. {}", idx + 1, text)) + } + }; + + if self.is_git_repo { + lines.push(create_option( + 0, + TrustDirectorySelection::Trust, + "Yes, allow Codex to work in this folder without asking for approval", + )); + lines.push(create_option( + 1, + TrustDirectorySelection::DontTrust, + "No, ask me to approve edits and commands", + )); + } else { + lines.push(create_option( + 0, + TrustDirectorySelection::Trust, + "Allow Codex to work in this folder without asking for approval", + )); + lines.push(create_option( + 1, + TrustDirectorySelection::DontTrust, + "Require approval of edits and commands", + )); + } + lines.push(Line::from("")); + if let Some(error) = &self.error { + lines.push(Line::from(format!(" {error}")).fg(Color::Red)); + lines.push(Line::from("")); + } + lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM)); + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } +} + +impl KeyboardHandler for TrustDirectoryWidget { + fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Up | KeyCode::Char('k') => { + self.highlighted = TrustDirectorySelection::Trust; + } + KeyCode::Down | KeyCode::Char('j') => { + self.highlighted = TrustDirectorySelection::DontTrust; + } + KeyCode::Char('1') => self.handle_trust(), + KeyCode::Char('2') => self.handle_dont_trust(), + KeyCode::Enter => match self.highlighted { + TrustDirectorySelection::Trust => self.handle_trust(), + TrustDirectorySelection::DontTrust => self.handle_dont_trust(), + }, + _ => {} + } + } +} + +impl StepStateProvider for TrustDirectoryWidget { + fn get_step_state(&self) -> StepState { + match self.selection { + Some(_) => StepState::Complete, + None => StepState::InProgress, + } + } +} + +impl TrustDirectoryWidget { + fn handle_trust(&mut self) { + if let Err(e) = set_project_trusted(&self.codex_home, &self.cwd) { + tracing::error!("Failed to set project trusted: {e:?}"); + self.error = Some(e.to_string()); + // self.error = Some("Failed to set project trusted".to_string()); + } + + // Update the in-memory chat config for this session to a more permissive + // policy suitable for a trusted workspace. + if let Ok(mut args) = self.chat_widget_args.lock() { + args.config.approval_policy = AskForApproval::OnRequest; + args.config.sandbox_policy = SandboxPolicy::new_workspace_write_policy(); + } + + self.selection = Some(TrustDirectorySelection::Trust); + } + + fn handle_dont_trust(&mut self) { + self.highlighted = TrustDirectorySelection::DontTrust; + self.selection = Some(TrustDirectorySelection::DontTrust); + } +} diff --git a/codex-rs/tui/src/onboarding/welcome.rs b/codex-rs/tui/src/onboarding/welcome.rs new file mode 100644 index 0000000000..bcdc5c9aae --- /dev/null +++ b/codex-rs/tui/src/onboarding/welcome.rs @@ -0,0 +1,38 @@ +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::prelude::Widget; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::text::Line; +use ratatui::text::Span; +use ratatui::widgets::WidgetRef; + +use crate::onboarding::onboarding_screen::StepStateProvider; + +use super::onboarding_screen::StepState; + +pub(crate) struct WelcomeWidget { + pub is_logged_in: bool, +} + +impl WidgetRef for &WelcomeWidget { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + let line = Line::from(vec![ + Span::raw(">_ "), + Span::styled( + "Welcome to Codex, OpenAI's command-line coding agent", + Style::default().add_modifier(Modifier::BOLD), + ), + ]); + line.render(area, buf); + } +} + +impl StepStateProvider for WelcomeWidget { + fn get_step_state(&self) -> StepState { + match self.is_logged_in { + true => StepState::Hidden, + false => StepState::Complete, + } + } +} diff --git a/codex-rs/tui/src/shimmer.rs b/codex-rs/tui/src/shimmer.rs new file mode 100644 index 0000000000..9d28b732a0 --- /dev/null +++ b/codex-rs/tui/src/shimmer.rs @@ -0,0 +1,84 @@ +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::time::Duration; + +use ratatui::style::Color; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::text::Span; + +use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; + +#[derive(Debug)] +pub(crate) struct FrameTicker { + running: Arc, +} + +impl FrameTicker { + pub(crate) fn new(app_event_tx: AppEventSender) -> Self { + let running = Arc::new(AtomicBool::new(true)); + let running_clone = running.clone(); + let app_event_tx_clone = app_event_tx.clone(); + std::thread::spawn(move || { + while running_clone.load(Ordering::Relaxed) { + std::thread::sleep(Duration::from_millis(100)); + app_event_tx_clone.send(AppEvent::RequestRedraw); + } + }); + Self { running } + } +} + +impl Drop for FrameTicker { + fn drop(&mut self) { + self.running.store(false, Ordering::Relaxed); + } +} + +pub(crate) fn shimmer_spans(text: &str, frame_idx: usize) -> Vec> { + let chars: Vec = text.chars().collect(); + let padding = 10usize; + let period = chars.len() + padding * 2; + let pos = frame_idx % period; + let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout) + .map(|level| level.has_16m) + .unwrap_or(false); + let band_half_width = 6.0; + + let mut spans: Vec> = Vec::with_capacity(chars.len()); + for (i, ch) in chars.iter().enumerate() { + let i_pos = i as isize + padding as isize; + let pos = pos as isize; + let dist = (i_pos - pos).abs() as f32; + + let t = if dist <= band_half_width { + let x = std::f32::consts::PI * (dist / band_half_width); + 0.5 * (1.0 + x.cos()) + } else { + 0.0 + }; + let brightness = 0.4 + 0.6 * t; + let level = (brightness * 255.0).clamp(0.0, 255.0) as u8; + let style = if has_true_color { + Style::default() + .fg(Color::Rgb(level, level, level)) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(color_for_level(level)) + }; + spans.push(Span::styled(ch.to_string(), style)); + } + spans +} + +fn color_for_level(level: u8) -> Color { + if level < 128 { + Color::DarkGray + } else if level < 192 { + Color::Gray + } else { + Color::White + } +} diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index d82a16608f..e58ab8521e 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -13,8 +13,12 @@ pub enum SlashCommand { // DO NOT ALPHA-SORT! Enum order is presentation order in the popup, so // more frequently used commands should be listed first. New, + Init, Compact, Diff, + Status, + Prompts, + Logout, Quit, #[cfg(debug_assertions)] TestApproval, @@ -24,14 +28,16 @@ impl SlashCommand { /// User-visible description shown in the popup. pub fn description(self) -> &'static str { match self { - SlashCommand::New => "Start a new chat.", - SlashCommand::Compact => "Compact the chat history.", - SlashCommand::Quit => "Exit the application.", - SlashCommand::Diff => { - "Show git diff of the working directory (including untracked files)" - } + SlashCommand::New => "start a new chat during a conversation", + SlashCommand::Init => "create an AGENTS.md file with instructions for Codex", + SlashCommand::Compact => "summarize conversation to prevent hitting the context limit", + SlashCommand::Quit => "exit Codex", + SlashCommand::Diff => "show git diff (including untracked files)", + SlashCommand::Status => "show current session configuration and token usage", + SlashCommand::Prompts => "show example prompts", + SlashCommand::Logout => "log out of Codex", #[cfg(debug_assertions)] - SlashCommand::TestApproval => "Test approval request", + SlashCommand::TestApproval => "test approval request", } } diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index 7e6d267481..6ef293208d 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -7,26 +7,25 @@ use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; +use std::time::Instant; use ratatui::buffer::Buffer; -use ratatui::layout::Alignment; 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::Block; -use ratatui::widgets::BorderType; -use ratatui::widgets::Borders; -use ratatui::widgets::Padding; use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; +use unicode_width::UnicodeWidthStr; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; +// We render the live text using markdown so it visually matches the history +// cells. Before rendering we strip any ANSI escape sequences to avoid writing +// raw control bytes into the back buffer. use codex_ansi_escape::ansi_escape_line; pub(crate) struct StatusIndicatorWidget { @@ -34,8 +33,17 @@ pub(crate) struct StatusIndicatorWidget { /// time). text: String, + /// Animation state: reveal target `text` progressively like a typewriter. + /// We compute the currently visible prefix length based on the current + /// frame index and a constant typing speed. The `base_frame` and + /// `reveal_len_at_base` form the anchor from which we advance. + last_target_len: usize, + base_frame: usize, + reveal_len_at_base: usize, + frame_idx: Arc, running: Arc, + start_time: Instant, // Keep one sender alive to prevent the channel from closing while the // animation thread is still running. The field itself is currently not // accessed anywhere, therefore the leading underscore silences the @@ -57,7 +65,7 @@ impl StatusIndicatorWidget { thread::spawn(move || { let mut counter = 0usize; while running_clone.load(Ordering::Relaxed) { - std::thread::sleep(Duration::from_millis(200)); + std::thread::sleep(Duration::from_millis(100)); counter = counter.wrapping_add(1); frame_idx_clone.store(counter, Ordering::Relaxed); app_event_tx_clone.send(AppEvent::RequestRedraw); @@ -66,9 +74,14 @@ impl StatusIndicatorWidget { } Self { - text: String::from("waiting for logs…"), + text: String::from("waiting for model"), + last_target_len: 0, + base_frame: 0, + reveal_len_at_base: 0, frame_idx, running, + start_time: Instant::now(), + _app_event_tx: app_event_tx, } } @@ -79,7 +92,67 @@ impl StatusIndicatorWidget { /// Update the line that is displayed in the widget. pub(crate) fn update_text(&mut self, text: String) { - self.text = text.replace(['\n', '\r'], " "); + // If the text hasn't changed, don't reset the baseline; let the + // animation continue advancing naturally. + if text == self.text { + return; + } + // Update the target text, preserving newlines so wrapping matches history cells. + // Strip ANSI escapes for the character count so the typewriter animation speed is stable. + let stripped = { + let line = ansi_escape_line(&text); + line.spans + .iter() + .map(|s| s.content.as_ref()) + .collect::>() + .join("") + }; + let new_len = stripped.chars().count(); + + // Compute how many characters are currently revealed so we can carry + // this forward as the new baseline when target text changes. + let current_frame = self.frame_idx.load(std::sync::atomic::Ordering::Relaxed); + let shown_now = self.current_shown_len(current_frame); + + self.text = text; + self.last_target_len = new_len; + self.base_frame = current_frame; + self.reveal_len_at_base = shown_now.min(new_len); + } + + /// Reset the animation and start revealing `text` from the beginning. + #[cfg(test)] + pub(crate) fn restart_with_text(&mut self, text: String) { + let sanitized = text.replace(['\n', '\r'], " "); + let stripped = { + let line = ansi_escape_line(&sanitized); + line.spans + .iter() + .map(|s| s.content.as_ref()) + .collect::>() + .join("") + }; + + let new_len = stripped.chars().count(); + let current_frame = self.frame_idx.load(std::sync::atomic::Ordering::Relaxed); + + self.text = sanitized; + self.last_target_len = new_len; + self.base_frame = current_frame; + // Start from zero revealed characters for a fresh typewriter cycle. + self.reveal_len_at_base = 0; + } + + /// Calculate how many characters should currently be visible given the + /// animation baseline and frame counter. + fn current_shown_len(&self, current_frame: usize) -> usize { + // Increase typewriter speed (~5x): reveal more characters per frame. + const TYPING_CHARS_PER_FRAME: usize = 7; + let frames = current_frame.saturating_sub(self.base_frame); + let advanced = self + .reveal_len_at_base + .saturating_add(frames.saturating_mul(TYPING_CHARS_PER_FRAME)); + advanced.min(self.last_target_len) } } @@ -92,100 +165,193 @@ impl Drop for StatusIndicatorWidget { impl WidgetRef for StatusIndicatorWidget { fn render_ref(&self, area: Rect, buf: &mut Buffer) { - let widget_style = Style::default(); - let block = Block::default() - .padding(Padding::new(1, 0, 0, 0)) - .borders(Borders::LEFT) - .border_type(BorderType::QuadrantOutside) - .border_style(widget_style.dim()); - // Animated 3‑dot pattern inside brackets. The *active* dot is bold - // white, the others are dim. - const DOT_COUNT: usize = 3; + // Ensure minimal height + if area.height == 0 || area.width == 0 { + return; + } + let idx = self.frame_idx.load(std::sync::atomic::Ordering::Relaxed); - let phase = idx % (DOT_COUNT * 2 - 2); - let active = if phase < DOT_COUNT { - phase - } else { - (DOT_COUNT * 2 - 2) - phase - }; + let elapsed = self.start_time.elapsed().as_secs(); + let shown_now = self.current_shown_len(idx); + let status_prefix: String = self.text.chars().take(shown_now).collect(); + let animated_text = "Working"; + let header_chars: Vec = animated_text.chars().collect(); + let padding = 4usize; // virtual padding around the animated segment for smoother loop + let period = header_chars.len() + padding * 2; + let pos = idx % period; + let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout) + .map(|level| level.has_16m) + .unwrap_or(false); + let band_half_width = 2.0; // width of the bright band in characters - let mut header_spans: Vec> = Vec::new(); + let mut animated_spans: Vec> = Vec::new(); + for (i, ch) in header_chars.iter().enumerate() { + let i_pos = i as isize + padding as isize; + let pos = pos as isize; + let dist = (i_pos - pos).abs() as f32; - header_spans.push(Span::styled( - "Working ", - Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD), - )); + let t = if dist <= band_half_width { + let x = std::f32::consts::PI * (dist / band_half_width); + 0.5 * (1.0 + x.cos()) + } else { + 0.0 + }; - header_spans.push(Span::styled( - "[", - Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD), - )); - - for i in 0..DOT_COUNT { - let style = if i == active { + let brightness = 0.4 + 0.6 * t; + let level = (brightness * 255.0).clamp(0.0, 255.0) as u8; + let style = if has_true_color { Style::default() - .fg(Color::White) + .fg(Color::Rgb(level, level, level)) .add_modifier(Modifier::BOLD) } else { - Style::default().dim() + Style::default().fg(color_for_level(level)) }; - header_spans.push(Span::styled(".", style)); + + animated_spans.push(Span::styled(ch.to_string(), style)); } - header_spans.push(Span::styled( - "] ", - Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD), + // Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback. + let inner_width = area.width as usize; + + let mut spans: Vec> = Vec::new(); + spans.push(Span::styled("▌ ", Style::default().fg(Color::Cyan))); + + // Simple dim spinner to the left of the header. + let spinner_frames = ['·', '•', '●', '•']; + const SPINNER_SLOWDOWN: usize = 2; + let spinner_ch = spinner_frames[(idx / SPINNER_SLOWDOWN) % spinner_frames.len()]; + spans.push(Span::styled( + spinner_ch.to_string(), + Style::default().fg(Color::DarkGray), )); + spans.push(Span::raw(" ")); - // Ensure we do not overflow width. - let inner_width = block.inner(area).width as usize; + // Space after header + // Animated header after the left bar + spans.extend(animated_spans); + // Space between header and bracket block + spans.push(Span::raw(" ")); + // Non-animated, dim bracket content, with keys bold + let bracket_prefix = format!("({elapsed}s • "); + spans.push(Span::styled( + bracket_prefix, + Style::default().fg(Color::Gray).add_modifier(Modifier::DIM), + )); + spans.push(Span::styled( + "Esc", + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::DIM | Modifier::BOLD), + )); + spans.push(Span::styled( + " to interrupt)", + Style::default().fg(Color::Gray).add_modifier(Modifier::DIM), + )); + // Add a space and then the log text (not animated by the gradient) + if !status_prefix.is_empty() { + spans.push(Span::styled( + " ", + Style::default().fg(Color::Gray).add_modifier(Modifier::DIM), + )); + spans.push(Span::styled( + status_prefix, + Style::default().fg(Color::Gray).add_modifier(Modifier::DIM), + )); + } - // Sanitize and colour‑strip the potentially colourful log text. This - // ensures that **no** raw ANSI escape sequences leak into the - // back‑buffer which would otherwise cause cursor jumps or stray - // artefacts when the terminal is resized. - let line = ansi_escape_line(&self.text); - let mut sanitized_tail: String = line - .spans - .iter() - .map(|s| s.content.as_ref()) - .collect::>() - .join(""); - - // Truncate *after* stripping escape codes so width calculation is - // accurate. See UTF‑8 boundary comments above. - let header_len: usize = header_spans.iter().map(|s| s.content.len()).sum(); - - if header_len + sanitized_tail.len() > inner_width { - let available_bytes = inner_width.saturating_sub(header_len); - - if sanitized_tail.is_char_boundary(available_bytes) { - sanitized_tail.truncate(available_bytes); + // Truncate spans to fit the width. + let mut acc: Vec> = Vec::new(); + let mut used = 0usize; + for s in spans { + let w = s.content.width(); + if used + w <= inner_width { + acc.push(s); + used += w; } else { - let mut idx = available_bytes; - while idx < sanitized_tail.len() && !sanitized_tail.is_char_boundary(idx) { - idx += 1; - } - sanitized_tail.truncate(idx); + break; } } + let lines = vec![Line::from(acc)]; - let mut spans = header_spans; + // No-op once full text is revealed; the app no longer reacts to a completion event. - // Re‑apply the DIM modifier so the tail appears visually subdued - // irrespective of the colour information preserved by - // `ansi_escape_line`. - spans.push(Span::styled(sanitized_tail, Style::default().dim())); - - let paragraph = Paragraph::new(Line::from(spans)) - .block(block) - .alignment(Alignment::Left); + let paragraph = Paragraph::new(lines); paragraph.render_ref(area, buf); } } + +fn color_for_level(level: u8) -> Color { + if level < 128 { + Color::DarkGray + } else if level < 192 { + Color::Gray + } else { + Color::White + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::app_event::AppEvent; + use crate::app_event_sender::AppEventSender; + use std::sync::mpsc::channel; + + #[test] + fn renders_without_left_border_or_padding() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut w = StatusIndicatorWidget::new(tx); + w.restart_with_text("Hello".to_string()); + + let area = ratatui::layout::Rect::new(0, 0, 30, 1); + // Allow a short delay so the typewriter reveals the first character. + std::thread::sleep(std::time::Duration::from_millis(120)); + let mut buf = ratatui::buffer::Buffer::empty(area); + w.render_ref(area, &mut buf); + + // Leftmost column has the left bar + let ch0 = buf[(0, 0)].symbol().chars().next().unwrap_or(' '); + assert_eq!(ch0, '▌', "expected left bar at col 0: {ch0:?}"); + } + + #[test] + fn working_header_is_present_on_last_line() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut w = StatusIndicatorWidget::new(tx); + w.restart_with_text("Hi".to_string()); + // Ensure some frames elapse so we get a stable state. + std::thread::sleep(std::time::Duration::from_millis(120)); + + let area = ratatui::layout::Rect::new(0, 0, 30, 1); + let mut buf = ratatui::buffer::Buffer::empty(area); + w.render_ref(area, &mut buf); + + // Single line; it should contain the animated "Working" header. + let mut row = String::new(); + for x in 0..area.width { + row.push(buf[(x, 0)].symbol().chars().next().unwrap_or(' ')); + } + assert!(row.contains("Working"), "expected Working header: {row:?}"); + } + + #[test] + fn spinner_is_rendered() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut w = StatusIndicatorWidget::new(tx); + w.restart_with_text("Hello".to_string()); + std::thread::sleep(std::time::Duration::from_millis(120)); + + let area = ratatui::layout::Rect::new(0, 0, 30, 1); + let mut buf = ratatui::buffer::Buffer::empty(area); + w.render_ref(area, &mut buf); + + let ch = buf[(2, 0)].symbol().chars().next().unwrap_or(' '); + assert!( + matches!(ch, '·' | '•' | '●'), + "expected spinner char at col 2: {ch:?}" + ); + } +} diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index 268483cbcf..0447e32ae9 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -3,11 +3,14 @@ use std::io::Stdout; use std::io::stdout; use codex_core::config::Config; +use crossterm::cursor::MoveTo; use crossterm::event::DisableBracketedPaste; use crossterm::event::EnableBracketedPaste; use crossterm::event::KeyboardEnhancementFlags; use crossterm::event::PopKeyboardEnhancementFlags; use crossterm::event::PushKeyboardEnhancementFlags; +use crossterm::terminal::Clear; +use crossterm::terminal::ClearType; use ratatui::backend::CrosstermBackend; use ratatui::crossterm::execute; use ratatui::crossterm::terminal::disable_raw_mode; @@ -26,16 +29,22 @@ pub fn init(_config: &Config) -> Result { // Enable keyboard enhancement flags so modifiers for keys like Enter are disambiguated. // chat_composer.rs is using a keyboard event listener to enter for any modified keys // to create a new line that require this. - execute!( + // Some terminals (notably legacy Windows consoles) do not support + // keyboard enhancement flags. Attempt to enable them, but continue + // gracefully if unsupported. + let _ = execute!( stdout(), PushKeyboardEnhancementFlags( KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_EVENT_TYPES | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS ) - )?; + ); set_panic_hook(); + // Clear screen and move cursor to top-left before drawing UI + execute!(stdout(), Clear(ClearType::All), MoveTo(0, 0))?; + let backend = CrosstermBackend::new(stdout()); let tui = Terminal::with_options(backend)?; Ok(tui) @@ -51,7 +60,8 @@ fn set_panic_hook() { /// Restore the terminal to its original state pub fn restore() -> Result<()> { - execute!(stdout(), PopKeyboardEnhancementFlags)?; + // Pop may fail on platforms that didn't support the push; ignore errors. + let _ = execute!(stdout(), PopKeyboardEnhancementFlags); execute!(stdout(), DisableBracketedPaste)?; disable_raw_mode()?; Ok(()) diff --git a/codex-rs/tui/src/updates.rs b/codex-rs/tui/src/updates.rs new file mode 100644 index 0000000000..c7f7afd2a5 --- /dev/null +++ b/codex-rs/tui/src/updates.rs @@ -0,0 +1,137 @@ +#![cfg(any(not(debug_assertions), test))] + +use chrono::DateTime; +use chrono::Duration; +use chrono::Utc; +use serde::Deserialize; +use serde::Serialize; +use std::path::Path; +use std::path::PathBuf; + +use codex_core::config::Config; + +pub fn get_upgrade_version(config: &Config) -> Option { + let version_file = version_filepath(config); + let info = read_version_info(&version_file).ok(); + + if match &info { + None => true, + Some(info) => info.last_checked_at < Utc::now() - Duration::hours(20), + } { + // Refresh the cached latest version in the background so TUI startup + // isn’t blocked by a network call. The UI reads the previously cached + // value (if any) for this run; the next run shows the banner if needed. + tokio::spawn(async move { + check_for_update(&version_file) + .await + .inspect_err(|e| tracing::error!("Failed to update version: {e}")) + }); + } + + info.and_then(|info| { + let current_version = env!("CARGO_PKG_VERSION"); + if is_newer(&info.latest_version, current_version).unwrap_or(false) { + Some(info.latest_version) + } else { + None + } + }) +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct VersionInfo { + latest_version: String, + // ISO-8601 timestamp (RFC3339) + last_checked_at: DateTime, +} + +#[derive(Deserialize, Debug, Clone)] +struct ReleaseInfo { + tag_name: String, +} + +const VERSION_FILENAME: &str = "version.json"; +const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/openai/codex/releases/latest"; + +fn version_filepath(config: &Config) -> PathBuf { + config.codex_home.join(VERSION_FILENAME) +} + +fn read_version_info(version_file: &Path) -> anyhow::Result { + let contents = std::fs::read_to_string(version_file)?; + Ok(serde_json::from_str(&contents)?) +} + +async fn check_for_update(version_file: &Path) -> anyhow::Result<()> { + let ReleaseInfo { + tag_name: latest_tag_name, + } = reqwest::Client::new() + .get(LATEST_RELEASE_URL) + .header( + "User-Agent", + format!( + "codex/{} (+https://github.com/openai/codex)", + env!("CARGO_PKG_VERSION") + ), + ) + .send() + .await? + .error_for_status()? + .json::() + .await?; + + let info = VersionInfo { + latest_version: latest_tag_name + .strip_prefix("rust-v") + .ok_or_else(|| anyhow::anyhow!("Failed to parse latest tag name '{latest_tag_name}'"))? + .into(), + last_checked_at: Utc::now(), + }; + + let json_line = format!("{}\n", serde_json::to_string(&info)?); + if let Some(parent) = version_file.parent() { + tokio::fs::create_dir_all(parent).await?; + } + tokio::fs::write(version_file, json_line).await?; + Ok(()) +} + +fn is_newer(latest: &str, current: &str) -> Option { + match (parse_version(latest), parse_version(current)) { + (Some(l), Some(c)) => Some(l > c), + _ => None, + } +} + +fn parse_version(v: &str) -> Option<(u64, u64, u64)> { + let mut iter = v.trim().split('.'); + let maj = iter.next()?.parse::().ok()?; + let min = iter.next()?.parse::().ok()?; + let pat = iter.next()?.parse::().ok()?; + Some((maj, min, pat)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn prerelease_version_is_not_considered_newer() { + assert_eq!(is_newer("0.11.0-beta.1", "0.11.0"), None); + assert_eq!(is_newer("1.0.0-rc.1", "1.0.0"), None); + } + + #[test] + fn plain_semver_comparisons_work() { + assert_eq!(is_newer("0.11.1", "0.11.0"), Some(true)); + assert_eq!(is_newer("0.11.0", "0.11.1"), Some(false)); + assert_eq!(is_newer("1.0.0", "0.9.9"), Some(true)); + assert_eq!(is_newer("0.9.9", "1.0.0"), Some(false)); + } + + #[test] + fn whitespace_is_ignored() { + assert_eq!(parse_version(" 1.2.3 \n"), Some((1, 2, 3))); + assert_eq!(is_newer(" 1.2.3 ", "1.2.2"), Some(true)); + } +} diff --git a/codex-rs/tui/src/user_approval_widget.rs b/codex-rs/tui/src/user_approval_widget.rs index 91febde208..966b8d68f9 100644 --- a/codex-rs/tui/src/user_approval_widget.rs +++ b/codex-rs/tui/src/user_approval_widget.rs @@ -28,7 +28,6 @@ use ratatui::widgets::Wrap; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; -use crate::exec_command::relativize_to_home; use crate::exec_command::strip_bash_lc_and_escape; /// Request coming from the agent that needs user approval. @@ -36,6 +35,7 @@ pub(crate) enum ApprovalRequest { Exec { id: String, command: Vec, + #[allow(dead_code)] cwd: PathBuf, reason: Option, }, @@ -47,6 +47,8 @@ pub(crate) enum ApprovalRequest { } /// Options displayed in the *select* mode. +/// +/// The `key` is matched case-insensitively. struct SelectOption { label: Line<'static>, description: &'static str, @@ -113,21 +115,18 @@ impl UserApprovalWidget<'_> { pub(crate) fn new(approval_request: ApprovalRequest, app_event_tx: AppEventSender) -> Self { let confirmation_prompt = match &approval_request { ApprovalRequest::Exec { - command, - cwd, - reason, - .. + command, reason, .. } => { let cmd = strip_bash_lc_and_escape(command); - // Maybe try to relativize to the cwd of this process first? - // Will make cwd_str shorter in the common case. - let cwd_str = match relativize_to_home(cwd) { - Some(rel) => format!("~/{}", rel.display()), - None => cwd.display().to_string(), - }; + // Present a single-line summary without cwd: "codex wants to run: " + let mut cmd_span: Span = cmd.clone().into(); + cmd_span.style = cmd_span.style.add_modifier(Modifier::DIM); let mut contents: Vec = vec![ - Line::from(vec!["codex".bold().magenta(), " wants to run:".into()]), - Line::from(vec![cwd_str.dim(), "$".into(), format!(" {cmd}").into()]), + Line::from(vec![ + "? ".fg(Color::Blue), + "Codex wants to run ".bold(), + cmd_span, + ]), Line::from(""), ]; if let Some(reason) = reason { @@ -187,6 +186,16 @@ impl UserApprovalWidget<'_> { } } + /// Normalize a key for comparison. + /// - For `KeyCode::Char`, converts to lowercase for case-insensitive matching. + /// - Other key codes are returned unchanged. + fn normalize_keycode(code: KeyCode) -> KeyCode { + match code { + KeyCode::Char(c) => KeyCode::Char(c.to_ascii_lowercase()), + other => other, + } + } + /// Handle Ctrl-C pressed by the user while the modal is visible. /// Behaves like pressing Escape: abort the request and close the modal. pub(crate) fn on_ctrl_c(&mut self) { @@ -210,7 +219,12 @@ impl UserApprovalWidget<'_> { self.send_decision(ReviewDecision::Abort); } other => { - if let Some(opt) = self.select_options.iter().find(|opt| opt.key == other) { + let normalized = Self::normalize_keycode(other); + if let Some(opt) = self + .select_options + .iter() + .find(|opt| Self::normalize_keycode(opt.key) == normalized) + { self.send_decision(opt.decision); } } @@ -226,9 +240,52 @@ impl UserApprovalWidget<'_> { match &self.approval_request { ApprovalRequest::Exec { command, .. } => { let cmd = strip_bash_lc_and_escape(command); - lines.push(Line::from("approval decision")); - lines.push(Line::from(format!("$ {cmd}"))); - lines.push(Line::from(format!("decision: {decision:?}"))); + let mut cmd_span: Span = cmd.clone().into(); + cmd_span.style = cmd_span.style.add_modifier(Modifier::DIM); + + // Result line based on decision. + match decision { + ReviewDecision::Approved => { + lines.push(Line::from(vec![ + "✓ ".fg(Color::Green), + "You ".into(), + "approved".bold(), + " codex to run ".into(), + cmd_span, + " ".into(), + "this time".bold(), + ])); + } + ReviewDecision::ApprovedForSession => { + lines.push(Line::from(vec![ + "✓ ".fg(Color::Green), + "You ".into(), + "approved".bold(), + " codex to run ".into(), + cmd_span, + " ".into(), + "every time this session".bold(), + ])); + } + ReviewDecision::Denied => { + lines.push(Line::from(vec![ + "✗ ".fg(Color::Red), + "You ".into(), + "did not approve".bold(), + " codex to run ".into(), + cmd_span, + ])); + } + ReviewDecision::Abort => { + lines.push(Line::from(vec![ + "✗ ".fg(Color::Red), + "You ".into(), + "canceled".bold(), + " the request to run ".into(), + cmd_span, + ])); + } + } } ApprovalRequest::ApplyPatch { .. } => { lines.push(Line::from(format!("patch approval decision: {decision:?}"))); @@ -330,3 +387,59 @@ impl WidgetRef for &UserApprovalWidget<'_> { ); } } + +#[cfg(test)] +mod tests { + use super::*; + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + use std::path::PathBuf; + use std::sync::mpsc::channel; + + #[test] + fn lowercase_shortcut_is_accepted() { + let (tx_raw, rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let req = ApprovalRequest::Exec { + id: "1".to_string(), + command: vec!["echo".to_string()], + cwd: PathBuf::new(), + reason: None, + }; + let mut widget = UserApprovalWidget::new(req, tx); + widget.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); + assert!(widget.is_complete()); + let events: Vec = rx.try_iter().collect(); + assert!(events.iter().any(|e| matches!( + e, + AppEvent::CodexOp(Op::ExecApproval { + decision: ReviewDecision::Approved, + .. + }) + ))); + } + + #[test] + fn uppercase_shortcut_is_accepted() { + let (tx_raw, rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let req = ApprovalRequest::Exec { + id: "2".to_string(), + command: vec!["echo".to_string()], + cwd: PathBuf::new(), + reason: None, + }; + let mut widget = UserApprovalWidget::new(req, tx); + widget.handle_key_event(KeyEvent::new(KeyCode::Char('Y'), KeyModifiers::NONE)); + assert!(widget.is_complete()); + let events: Vec = rx.try_iter().collect(); + assert!(events.iter().any(|e| matches!( + e, + AppEvent::CodexOp(Op::ExecApproval { + decision: ReviewDecision::Approved, + .. + }) + ))); + } +} diff --git a/codex-rs/tui/tests/vt100_history.rs b/codex-rs/tui/tests/vt100_history.rs new file mode 100644 index 0000000000..11ee044041 --- /dev/null +++ b/codex-rs/tui/tests/vt100_history.rs @@ -0,0 +1,214 @@ +#![cfg(feature = "vt100-tests")] +#![expect(clippy::expect_used)] + +use ratatui::backend::TestBackend; +use ratatui::layout::Rect; +use ratatui::style::Color; +use ratatui::style::Style; +use ratatui::text::Line; +use ratatui::text::Span; + +// Small helper macro to assert a collection contains an item with a clearer +// failure message. +macro_rules! assert_contains { + ($collection:expr, $item:expr $(,)?) => { + assert!( + $collection.contains(&$item), + "Expected {:?} to contain {:?}", + $collection, + $item + ); + }; + ($collection:expr, $item:expr, $($arg:tt)+) => { + assert!($collection.contains(&$item), $($arg)+); + }; +} + +struct TestScenario { + width: u16, + height: u16, + term: codex_tui::custom_terminal::Terminal, +} + +impl TestScenario { + fn new(width: u16, height: u16, viewport: Rect) -> Self { + let backend = TestBackend::new(width, height); + let mut term = codex_tui::custom_terminal::Terminal::with_options(backend) + .expect("failed to construct terminal"); + term.set_viewport_area(viewport); + Self { + width, + height, + term, + } + } + + fn run_insert(&mut self, lines: Vec>) -> Vec { + let mut buf: Vec = Vec::new(); + codex_tui::insert_history::insert_history_lines_to_writer(&mut self.term, &mut buf, lines); + buf + } + + fn screen_rows_from_bytes(&self, bytes: &[u8]) -> Vec { + let mut parser = vt100::Parser::new(self.height, self.width, 0); + parser.process(bytes); + let screen = parser.screen(); + + let mut rows: Vec = Vec::with_capacity(self.height as usize); + for row in 0..self.height { + let mut s = String::with_capacity(self.width as usize); + for col in 0..self.width { + if let Some(cell) = screen.cell(row, col) { + if let Some(ch) = cell.contents().chars().next() { + s.push(ch); + } else { + s.push(' '); + } + } else { + s.push(' '); + } + } + rows.push(s.trim_end().to_string()); + } + rows + } +} + +#[test] +fn hist_001_basic_insertion_no_wrap() { + // Screen of 20x6; viewport is the last row (height=1 at y=5) + let area = Rect::new(0, 5, 20, 1); + let mut scenario = TestScenario::new(20, 6, area); + + let lines = vec![Line::from("first"), Line::from("second")]; + let buf = scenario.run_insert(lines); + let rows = scenario.screen_rows_from_bytes(&buf); + assert_contains!(rows, String::from("first")); + assert_contains!(rows, String::from("second")); + let first_idx = rows + .iter() + .position(|r| r == "first") + .expect("expected 'first' row to be present"); + let second_idx = rows + .iter() + .position(|r| r == "second") + .expect("expected 'second' row to be present"); + assert_eq!(second_idx, first_idx + 1, "rows should be adjacent"); +} + +#[test] +fn hist_002_long_token_wraps() { + let area = Rect::new(0, 5, 20, 1); + let mut scenario = TestScenario::new(20, 6, area); + + let long = "A".repeat(45); // > 2 lines at width 20 + let lines = vec![Line::from(long.clone())]; + let buf = scenario.run_insert(lines); + let mut parser = vt100::Parser::new(6, 20, 0); + parser.process(&buf); + let screen = parser.screen(); + + // Count total A's on the screen + let mut count_a = 0usize; + for row in 0..6 { + for col in 0..20 { + if let Some(cell) = screen.cell(row, col) { + if let Some(ch) = cell.contents().chars().next() { + if ch == 'A' { + count_a += 1; + } + } + } + } + } + + assert_eq!( + count_a, + long.len(), + "wrapped content did not preserve all characters" + ); +} + +#[test] +fn hist_003_emoji_and_cjk() { + let area = Rect::new(0, 5, 20, 1); + let mut scenario = TestScenario::new(20, 6, area); + + let text = String::from("😀😀😀😀😀 你好世界"); + let lines = vec![Line::from(text.clone())]; + let buf = scenario.run_insert(lines); + let rows = scenario.screen_rows_from_bytes(&buf); + let reconstructed: String = rows.join("").chars().filter(|c| *c != ' ').collect(); + for ch in text.chars().filter(|c| !c.is_whitespace()) { + assert!( + reconstructed.contains(ch), + "missing character {ch:?} in reconstructed screen" + ); + } +} + +#[test] +fn hist_004_mixed_ansi_spans() { + let area = Rect::new(0, 5, 20, 1); + let mut scenario = TestScenario::new(20, 6, area); + + let line = Line::from(vec![ + Span::styled("red", Style::default().fg(Color::Red)), + Span::raw("+plain"), + ]); + let buf = scenario.run_insert(vec![line]); + let rows = scenario.screen_rows_from_bytes(&buf); + assert_contains!(rows, String::from("red+plain")); +} + +#[test] +fn hist_006_cursor_restoration() { + let area = Rect::new(0, 5, 20, 1); + let mut scenario = TestScenario::new(20, 6, area); + + let lines = vec![Line::from("x")]; + let buf = scenario.run_insert(lines); + let s = String::from_utf8_lossy(&buf); + // CUP to 1;1 (ANSI: ESC[1;1H) + assert!( + s.contains("\u{1b}[1;1H"), + "expected final CUP to 1;1 in output, got: {s:?}" + ); + // Reset scroll region + assert!( + s.contains("\u{1b}[r"), + "expected reset scroll region in output, got: {s:?}" + ); +} + +#[test] +fn hist_005_pre_scroll_region_down() { + // Viewport not at bottom: y=3 (0-based), height=1 + let area = Rect::new(0, 3, 20, 1); + let mut scenario = TestScenario::new(20, 6, area); + + let lines = vec![Line::from("first"), Line::from("second")]; + let buf = scenario.run_insert(lines); + let s = String::from_utf8_lossy(&buf); + // Expect we limited scroll region to [top+1 .. screen_height] => [4 .. 6] (1-based) + assert!( + s.contains("\u{1b}[4;6r"), + "expected pre-scroll SetScrollRegion 4..6, got: {s:?}" + ); + // Expect we moved cursor to top of that region: row 3 (0-based) => CUP 4;1H + assert!( + s.contains("\u{1b}[4;1H"), + "expected cursor at top of pre-scroll region, got: {s:?}" + ); + // Expect at least two Reverse Index commands (ESC M) for two inserted lines + let ri_count = s.matches("\u{1b}M").count(); + assert!( + ri_count >= 1, + "expected at least one RI (ESC M), got: {s:?}" + ); + // After pre-scroll, we set insertion scroll region to [1 .. new_top] => [1 .. 5] + assert!( + s.contains("\u{1b}[1;5r"), + "expected insertion SetScrollRegion 1..5, got: {s:?}" + ); +} diff --git a/codex-rs/tui/tests/vt100_live_commit.rs b/codex-rs/tui/tests/vt100_live_commit.rs new file mode 100644 index 0000000000..c0cfb3211a --- /dev/null +++ b/codex-rs/tui/tests/vt100_live_commit.rs @@ -0,0 +1,101 @@ +#![cfg(feature = "vt100-tests")] + +use ratatui::backend::TestBackend; +use ratatui::layout::Rect; +use ratatui::text::Line; + +#[test] +fn live_001_commit_on_overflow() { + let backend = TestBackend::new(20, 6); + let mut term = match codex_tui::custom_terminal::Terminal::with_options(backend) { + Ok(t) => t, + Err(e) => panic!("failed to construct terminal: {e}"), + }; + let area = Rect::new(0, 5, 20, 1); + term.set_viewport_area(area); + + // Build 5 explicit rows at width 20. + let mut rb = codex_tui::live_wrap::RowBuilder::new(20); + rb.push_fragment("one\n"); + rb.push_fragment("two\n"); + rb.push_fragment("three\n"); + rb.push_fragment("four\n"); + rb.push_fragment("five\n"); + + // Keep the last 3 in the live ring; commit the first 2. + let commit_rows = rb.drain_commit_ready(3); + let lines: Vec> = commit_rows + .into_iter() + .map(|r| Line::from(r.text)) + .collect(); + + let mut buf: Vec = Vec::new(); + codex_tui::insert_history::insert_history_lines_to_writer(&mut term, &mut buf, lines); + + let mut parser = vt100::Parser::new(6, 20, 0); + parser.process(&buf); + let screen = parser.screen(); + + // The words "one" and "two" should appear above the viewport. + let mut joined = String::new(); + for row in 0..6 { + for col in 0..20 { + if let Some(cell) = screen.cell(row, col) { + if let Some(ch) = cell.contents().chars().next() { + joined.push(ch); + } else { + joined.push(' '); + } + } + } + joined.push('\n'); + } + assert!( + joined.contains("one"), + "expected committed 'one' to be visible\n{joined}" + ); + assert!( + joined.contains("two"), + "expected committed 'two' to be visible\n{joined}" + ); + // The last three (three,four,five) remain in the live ring, not committed here. +} + +#[test] +fn live_002_pre_scroll_and_commit() { + let backend = TestBackend::new(20, 6); + let mut term = match codex_tui::custom_terminal::Terminal::with_options(backend) { + Ok(t) => t, + Err(e) => panic!("failed to construct terminal: {e}"), + }; + // Viewport not at bottom: y=3 + let area = Rect::new(0, 3, 20, 1); + term.set_viewport_area(area); + + let mut rb = codex_tui::live_wrap::RowBuilder::new(20); + rb.push_fragment("alpha\n"); + rb.push_fragment("beta\n"); + rb.push_fragment("gamma\n"); + rb.push_fragment("delta\n"); + + // Keep 3, commit 1. + let commit_rows = rb.drain_commit_ready(3); + let lines: Vec> = commit_rows + .into_iter() + .map(|r| Line::from(r.text)) + .collect(); + + let mut buf: Vec = Vec::new(); + codex_tui::insert_history::insert_history_lines_to_writer(&mut term, &mut buf, lines); + let s = String::from_utf8_lossy(&buf); + + // Expect a SetScrollRegion to [area.top()+1 .. screen_height] and a cursor move to top of that region. + assert!( + s.contains("\u{1b}[4;6r"), + "expected pre-scroll region 4..6, got: {s:?}" + ); + assert!( + s.contains("\u{1b}[4;1H"), + "expected cursor CUP 4;1H, got: {s:?}" + ); +} diff --git a/docs/release_management.md b/docs/release_management.md new file mode 100644 index 0000000000..1b81bc3eb2 --- /dev/null +++ b/docs/release_management.md @@ -0,0 +1,47 @@ +# Release Management + +Currently, we made Codex binaries available in three places: + +- GitHub Releases https://github.com/openai/codex/releases/ +- `@openai/codex` on npm: https://www.npmjs.com/package/@openai/codex +- `codex` on Homebrew: https://formulae.brew.sh/formula/codex + +# Cutting a Release + +Currently, choosing the version number for the next release is a manual process. In general, just go to https://github.com/openai/codex/releases/latest and see what the latest release is and increase the minor version by `1`, so if the current release is `0.20.0`, then the next release should be `0.21.0`. + +Assuming you are trying to publish `0.21.0`, first you would run: + +```shell +VERSION=0.21.0 +./codex-rs/scripts/create_github_release.sh "$VERSION" +``` + +This will kick off a GitHub Action to build the release, so go to https://github.com/openai/codex/actions/workflows/rust-release.yml to find the corresponding workflow. (Note: we should automate finding the workflow URL with `gh`.) + +When the workflow finishes, the GitHub Release is "done," but you still have to consider npm and Homebrew. + +## Publishing to npm + +After the GitHub Release is done, you can publish to npm. Note the GitHub Release includes the appropriate artifact for npm (which is the output of `npm pack`), which should be named `codex-npm-VERSION.tgz`. To publish to npm, run: + +``` +VERSION=0.21.0 +./scripts/publish_to_npm.py "$VERSION" +``` + +Note that you must have permissions to publish to https://www.npmjs.com/package/@openai/codex for this to succeed. + +## Publishing to Homebrew + +For Homebrew, we are properly set up with their automation system, so every few hours or so it will check our GitHub repo to see if there is a new release. When it finds one, it will put up a PR to create the equivalent Homebrew release, which entails building Codex CLI from source on various versions of macOS. + +Inevitably, you just have to refresh this page periodically to see if the release has been picked up by their automation system: + +https://github.com/Homebrew/homebrew-core/pulls?q=%3Apr+codex + +Once everything builds, a Homebrew admin has to approve the PR. Again, the whole process takes several hours and we don't have total control over it, but it seems to work pretty well. + +For reference, our Homebrew formula lives at: + +https://github.com/Homebrew/homebrew-core/blob/main/Formula/c/codex.rb diff --git a/package.json b/package.json index 9d45c6e383..c13e6cb314 100644 --- a/package.json +++ b/package.json @@ -3,22 +3,10 @@ "private": true, "description": "Tools for repo-wide maintenance.", "scripts": { - "release": "pnpm --filter @openai/codex run release", "format": "prettier --check *.json *.md .github/workflows/*.yml", - "format:fix": "prettier --write *.json *.md .github/workflows/*.yml", - "build": "pnpm --filter @openai/codex run build", - "test": "pnpm --filter @openai/codex run test", - "lint": "pnpm --filter @openai/codex run lint", - "lint:fix": "pnpm --filter @openai/codex run lint:fix", - "typecheck": "pnpm --filter @openai/codex run typecheck", - "changelog": "git-cliff --config cliff.toml --output CHANGELOG.ignore.md $LAST_RELEASE_TAG..HEAD", - "prepare": "husky", - "husky:add": "husky add" + "format:fix": "prettier --write *.json *.md .github/workflows/*.yml" }, "devDependencies": { - "git-cliff": "^2.8.0", - "husky": "^9.1.7", - "lint-staged": "^15.5.1", "prettier": "^3.5.3" }, "resolutions": { @@ -29,24 +17,9 @@ "overrides": { "punycode": "^2.3.1" }, - "pnpm": { - "patchedDependencies": { - "marked-terminal@7.3.0": "patches/marked-terminal@7.3.0.patch" - } - }, "engines": { "node": ">=22", "pnpm": ">=9.0.0" }, - "lint-staged": { - "*.json": "prettier --write", - "*.md": "prettier --write", - ".github/workflows/*.yml": "prettier --write", - "**/*.{js,ts,tsx}": [ - "prettier --write", - "pnpm --filter @openai/codex run lint", - "cd codex-cli && pnpm run typecheck" - ] - }, "packageManager": "pnpm@10.8.1" } diff --git a/patches/marked-terminal@7.3.0.patch b/patches/marked-terminal@7.3.0.patch deleted file mode 100644 index bce52c34bc..0000000000 --- a/patches/marked-terminal@7.3.0.patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/index.js b/index.js -index 5e2d4b4f212a7c614ebcd5cba8c4928fa3e0d2d0..24dba3560bee4f88dac9106911ef204f37babebe 100644 ---- a/index.js -+++ b/index.js -@@ -83,7 +83,7 @@ Renderer.prototype.space = function () { - - Renderer.prototype.text = function (text) { - if (typeof text === 'object') { -- text = text.text; -+ text = text.tokens ? this.parser.parseInline(text.tokens) : text.text; - } - return this.o.text(text); - }; -@@ -185,10 +185,10 @@ Renderer.prototype.listitem = function (text) { - } - var transform = compose(this.o.listitem, this.transform); - var isNested = text.indexOf('\n') !== -1; -- if (isNested) text = text.trim(); -+ if (!isNested) text = transform(text); - - // Use BULLET_POINT as a marker for ordered or unordered list item -- return '\n' + BULLET_POINT + transform(text); -+ return '\n' + BULLET_POINT + text; - }; - - Renderer.prototype.checkbox = function (checked) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e4db69525..732e02b0bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,5418 +9,21 @@ overrides: micromatch: ^4.0.8 semver: ^7.7.1 -patchedDependencies: - marked-terminal@7.3.0: - hash: 536fe9685e91d559cf29a033191aa39da45729949e9d1c69989255091c8618fb - path: patches/marked-terminal@7.3.0.patch - importers: .: devDependencies: - git-cliff: - specifier: ^2.8.0 - version: 2.8.0 - husky: - specifier: ^9.1.7 - version: 9.1.7 - lint-staged: - specifier: ^15.5.1 - version: 15.5.1 prettier: specifier: ^3.5.3 version: 3.5.3 - codex-cli: - dependencies: - '@inkjs/ui': - specifier: ^2.0.0 - version: 2.0.0(ink@5.2.0(@types/react@18.3.20)(react@18.3.1)) - chalk: - specifier: ^5.2.0 - version: 5.4.1 - diff: - specifier: ^7.0.0 - version: 7.0.0 - dotenv: - specifier: ^16.1.4 - version: 16.5.0 - express: - specifier: ^5.1.0 - version: 5.1.0 - fast-deep-equal: - specifier: ^3.1.3 - version: 3.1.3 - fast-npm-meta: - specifier: ^0.4.2 - version: 0.4.2 - figures: - specifier: ^6.1.0 - version: 6.1.0 - file-type: - specifier: ^20.1.0 - version: 20.4.1 - https-proxy-agent: - specifier: ^7.0.6 - version: 7.0.6 - ink: - specifier: ^5.2.0 - version: 5.2.0(@types/react@18.3.20)(react@18.3.1) - js-yaml: - specifier: ^4.1.0 - version: 4.1.0 - marked: - specifier: ^15.0.7 - version: 15.0.8 - marked-terminal: - specifier: ^7.3.0 - version: 7.3.0(patch_hash=536fe9685e91d559cf29a033191aa39da45729949e9d1c69989255091c8618fb)(marked@15.0.8) - meow: - specifier: ^13.2.0 - version: 13.2.0 - open: - specifier: ^10.1.0 - version: 10.1.1 - openai: - specifier: ^4.95.1 - version: 4.95.1(ws@8.18.1)(zod@3.24.3) - package-manager-detector: - specifier: ^1.2.0 - version: 1.2.0 - react: - specifier: ^18.2.0 - version: 18.3.1 - shell-quote: - specifier: ^1.8.2 - version: 1.8.2 - strip-ansi: - specifier: ^7.1.0 - version: 7.1.0 - to-rotated: - specifier: ^1.0.0 - version: 1.0.0 - use-interval: - specifier: 1.4.0 - version: 1.4.0(react@18.3.1) - zod: - specifier: ^3.24.3 - version: 3.24.3 - devDependencies: - '@eslint/js': - specifier: ^9.22.0 - version: 9.24.0 - '@types/diff': - specifier: ^7.0.2 - version: 7.0.2 - '@types/express': - specifier: ^5.0.1 - version: 5.0.1 - '@types/js-yaml': - specifier: ^4.0.9 - version: 4.0.9 - '@types/marked-terminal': - specifier: ^6.1.1 - version: 6.1.1 - '@types/react': - specifier: ^18.0.32 - version: 18.3.20 - '@types/semver': - specifier: ^7.7.0 - version: 7.7.0 - '@types/shell-quote': - specifier: ^1.7.5 - version: 1.7.5 - '@types/which': - specifier: ^3.0.4 - version: 3.0.4 - '@typescript-eslint/eslint-plugin': - specifier: ^7.18.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/parser': - specifier: ^7.18.0 - version: 7.18.0(eslint@8.57.1)(typescript@5.8.3) - boxen: - specifier: ^8.0.1 - version: 8.0.1 - esbuild: - specifier: ^0.25.2 - version: 0.25.2 - eslint-plugin-import: - specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1) - eslint-plugin-react: - specifier: ^7.32.2 - version: 7.37.5(eslint@8.57.1) - eslint-plugin-react-hooks: - specifier: ^4.6.0 - version: 4.6.2(eslint@8.57.1) - eslint-plugin-react-refresh: - specifier: ^0.4.19 - version: 0.4.19(eslint@8.57.1) - husky: - specifier: ^9.1.7 - version: 9.1.7 - ink-testing-library: - specifier: ^3.0.0 - version: 3.0.0(@types/react@18.3.20) - prettier: - specifier: ^3.5.3 - version: 3.5.3 - punycode: - specifier: ^2.3.1 - version: 2.3.1 - semver: - specifier: ^7.7.1 - version: 7.7.1 - ts-node: - specifier: ^10.9.1 - version: 10.9.2(@types/node@22.14.1)(typescript@5.8.3) - typescript: - specifier: ^5.0.3 - version: 5.8.3 - vite: - specifier: ^6.3.4 - version: 6.3.4(@types/node@22.14.1)(yaml@2.7.1) - vitest: - specifier: ^3.1.2 - version: 3.1.2(@types/node@22.14.1)(yaml@2.7.1) - whatwg-url: - specifier: ^14.2.0 - version: 14.2.0 - which: - specifier: ^5.0.0 - version: 5.0.0 - packages: - '@alcalzone/ansi-tokenize@0.1.3': - resolution: {integrity: sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==} - engines: {node: '>=14.13.1'} - - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - - '@esbuild/aix-ppc64@0.25.2': - resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.2': - resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.2': - resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.25.2': - resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.2': - resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.2': - resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.25.2': - resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.2': - resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.25.2': - resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.2': - resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.25.2': - resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.2': - resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.2': - resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.2': - resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.2': - resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.2': - resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.25.2': - resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.2': - resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.2': - resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.25.2': - resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.2': - resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.25.2': - resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.25.2': - resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.25.2': - resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.2': - resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.6.1': - resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==} - 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/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@eslint/js@9.24.0': - resolution: {integrity: sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead - - '@inkjs/ui@2.0.0': - resolution: {integrity: sha512-5+8fJmwtF9UvikzLfph9sA+LS+l37Ij/szQltkuXLOAXwNkBX9innfzh4pLGXIB59vKEQUtc6D4qGvhD7h3pAg==} - engines: {node: '>=18'} - peerDependencies: - ink: '>=5' - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - - '@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'} - - '@rollup/rollup-android-arm-eabi@4.40.0': - resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.40.0': - resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.40.0': - resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.40.0': - resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.40.0': - resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.40.0': - resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.40.0': - resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.40.0': - resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.40.0': - resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.40.0': - resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loongarch64-gnu@4.40.0': - resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': - resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.40.0': - resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.40.0': - resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.40.0': - resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.40.0': - resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.40.0': - resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.40.0': - resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.40.0': - resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.40.0': - resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==} - cpu: [x64] - os: [win32] - - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - - '@sindresorhus/is@4.6.0': - resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} - engines: {node: '>=10'} - - '@tokenizer/inflate@0.2.7': - resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} - engines: {node: '>=18'} - - '@tokenizer/token@0.3.0': - resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - - '@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==} - - '@types/body-parser@1.19.5': - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - - '@types/cardinal@2.1.1': - resolution: {integrity: sha512-/xCVwg8lWvahHsV2wXZt4i64H1sdL+sN1Uoq7fAc8/FA6uYHjuIveDwPwvGUYp4VZiv85dVl6J/Bum3NDAOm8g==} - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@types/diff@7.0.2': - resolution: {integrity: sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==} - - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - - '@types/express-serve-static-core@5.0.6': - resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} - - '@types/express@5.0.1': - resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==} - - '@types/http-errors@2.0.4': - resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - - '@types/js-yaml@4.0.9': - resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - - '@types/marked-terminal@6.1.1': - resolution: {integrity: sha512-DfoUqkmFDCED7eBY9vFUhJ9fW8oZcMAK5EwRDQ9drjTbpQa+DnBTQQCwWhTFVf4WsZ6yYcJTI8D91wxTWXRZZQ==} - - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - - '@types/node-fetch@2.6.12': - resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} - - '@types/node@18.19.86': - resolution: {integrity: sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ==} - - '@types/node@22.14.1': - resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} - - '@types/prop-types@15.7.14': - resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - - '@types/qs@6.9.18': - resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - - '@types/react@18.3.20': - resolution: {integrity: sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==} - - '@types/semver@7.7.0': - resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} - - '@types/send@0.17.4': - resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} - - '@types/serve-static@1.15.7': - resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - - '@types/shell-quote@1.7.5': - resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} - - '@types/which@3.0.4': - resolution: {integrity: sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==} - - '@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} - - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - - '@vitest/expect@3.1.2': - resolution: {integrity: sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==} - - '@vitest/mocker@3.1.2': - resolution: {integrity: sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@3.1.2': - resolution: {integrity: sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==} - - '@vitest/runner@3.1.2': - resolution: {integrity: sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==} - - '@vitest/snapshot@3.1.2': - resolution: {integrity: sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==} - - '@vitest/spy@3.1.2': - resolution: {integrity: sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==} - - '@vitest/utils@3.1.2': - resolution: {integrity: sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==} - - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - - 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.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - - agent-base@7.1.3: - resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} - engines: {node: '>= 14'} - - agentkeepalive@4.6.0: - resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} - engines: {node: '>= 8.0.0'} - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-align@3.0.1: - resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - - ansi-escapes@7.0.0: - resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} - engines: {node: '>=18'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - - 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.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} - engines: {node: '>= 0.4'} - - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - - array.prototype.findlast@1.2.5: - resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} - 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'} - - array.prototype.tosorted@1.1.4: - resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - auto-bind@5.0.1: - resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - 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==} - - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} - engines: {node: '>=18'} - - boxen@8.0.1: - resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} - engines: {node: '>=18'} - - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - bundle-name@4.1.0: - resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} - engines: {node: '>=18'} - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - 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'} - - camelcase@8.0.0: - resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} - engines: {node: '>=16'} - - chai@5.2.0: - resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} - engines: {node: '>=12'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - chalk@5.4.1: - resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} - - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - - cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} - - cli-cursor@4.0.0: - resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - cli-cursor@5.0.0: - resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} - engines: {node: '>=18'} - - cli-highlight@2.1.11: - resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} - hasBin: true - - cli-spinners@3.2.0: - resolution: {integrity: sha512-pXftdQloMZzjCr3pCTIRniDcys6dDzgpgVhAHHk6TKBDbRuP1MkuetTF5KSv4YUutbOPa7+7ZrAJ2kVtbMqyXA==} - engines: {node: '>=18.20'} - - cli-table3@0.6.5: - resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} - engines: {node: 10.* || >= 12.*} - - cli-truncate@4.0.0: - resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} - engines: {node: '>=18'} - - cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - - code-excerpt@4.0.0: - resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.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==} - - colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - - commander@13.1.0: - resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} - engines: {node: '>=18'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - content-disposition@1.0.0: - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} - engines: {node: '>= 0.6'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - convert-to-spaces@2.0.1: - resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - 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'} - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - - 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.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - - 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'} - - default-browser-id@5.0.0: - resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} - engines: {node: '>=18'} - - default-browser@5.2.1: - resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} - engines: {node: '>=18'} - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-lazy-prop@3.0.0: - resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} - engines: {node: '>=12'} - - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - - diff@7.0.0: - resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} - engines: {node: '>=0.3.1'} - - 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'} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - - dotenv@16.5.0: - resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} - engines: {node: '>=12'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - emoji-regex@10.4.0: - resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emojilib@2.4.0: - resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - - environment@1.1.0: - resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} - engines: {node: '>=18'} - - es-abstract@1.23.9: - resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} - 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-iterator-helpers@1.2.1: - resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} - engines: {node: '>= 0.4'} - - es-module-lexer@1.6.0: - resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} - - 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'} - - es-toolkit@1.35.0: - resolution: {integrity: sha512-kVHyrRoC0eLc1hWJ6npG8nNFtOG+nWfcMI+XE0RaFO0gxd6Ions8r0O/U64QyZgY7IeidUnS5oZlRZYUgMGCAg==} - - esbuild@0.25.2: - resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - - 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'} - - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - - eslint-module-utils@2.12.0: - resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} - 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.31.0: - resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} - 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-plugin-react-hooks@4.6.2: - resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - - eslint-plugin-react-refresh@0.4.19: - resolution: {integrity: sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==} - peerDependencies: - eslint: '>=8.40' - - eslint-plugin-react@7.37.5: - resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.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@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.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'} - - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - - execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - - expect-type@1.2.1: - resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} - engines: {node: '>=12.0.0'} - - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} - engines: {node: '>= 18'} - - 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==} - - fast-npm-meta@0.4.2: - resolution: {integrity: sha512-BDN/yv8MN3fjh504wa7/niZojPtf/brWBsLKlw7Fv+Xh8Df+6ZEAFpp3zaal4etgDxxav1CuzKX5H0YVM9urEQ==} - - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - - fdir@6.4.4: - resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - - figures@6.1.0: - resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} - engines: {node: '>=18'} - - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - - file-type@20.4.1: - resolution: {integrity: sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==} - engines: {node: '>=18'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - finalhandler@2.1.0: - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} - engines: {node: '>= 0.8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - - 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'} - - form-data-encoder@1.7.2: - resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} - - form-data@4.0.2: - resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} - engines: {node: '>= 6'} - - formdata-node@4.4.1: - resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} - engines: {node: '>= 12.20'} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - - 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==} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-east-asian-width@1.3.0: - resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} - engines: {node: '>=18'} - - 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'} - - git-cliff-darwin-arm64@2.8.0: - resolution: {integrity: sha512-rurUV2d1Z2n+c2+wUrO0gZaFb3c1G+ej0bPfKTPfde/CblxiysMkh+4dz23NrVbc8IlS5rSYv/JFGVaVSBNJRw==} - cpu: [arm64] - os: [darwin] - - git-cliff-darwin-x64@2.8.0: - resolution: {integrity: sha512-Wtj+FGWZBWmeYUAGlkfz7QPz4+VVxxDPMhQ/7iwKVA3iryIX0slGfzYpqMurEFnTAMr0r+4IU3Q4O/ib7iUscg==} - cpu: [x64] - os: [darwin] - - git-cliff-linux-arm64@2.8.0: - resolution: {integrity: sha512-k4RdfMdORXyefznWlQb+7wDgo7XgQF9qg8hJC34bwyJK2sODirrGau3uTx1/9Fi37g+pAOM7wM+LYppHCTZ2bQ==} - cpu: [arm64] - os: [linux] - - git-cliff-linux-x64@2.8.0: - resolution: {integrity: sha512-FcWX4GHgodYrQlZR03fzooanStgR03JNWvyaMQB1asplQ18nlziK2UyA+PESCIxOQmeLXauqoCApfzmdtp5myg==} - cpu: [x64] - os: [linux] - - git-cliff-windows-arm64@2.8.0: - resolution: {integrity: sha512-GJSrqmBVTbMtBJI3/YCDxLviZZDgYgnKqYgquBk2u2AELAnnuWFnVFQ7ZEBUqgFF2UJu9EdV2Nv6MV8d/wnP0g==} - cpu: [arm64] - os: [win32] - - git-cliff-windows-x64@2.8.0: - resolution: {integrity: sha512-8jl0YMXPYjUmVygUEeQ4wf1zte3Rv8LPq1sIklUKl80XE4g2Gm/8EIWbKpUPLQH6IncRwepY6VuMgpVpPXbwNw==} - cpu: [x64] - os: [win32] - - git-cliff@2.8.0: - resolution: {integrity: sha512-iKF5QTXAb9+iVvmu5HpnMPWYw7fs74xkpAaRbSf29+dZaMTTNRIUST/y+Ir2S1bDUWWJNjXlwT9ZT62JuYLQnA==} - engines: {node: '>=18.19 || >=20.6 || >=21'} - hasBin: true - - 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@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - - 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'} - - highlight.js@10.7.3: - resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - - human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - - humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - - husky@9.1.7: - resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} - engines: {node: '>=18'} - hasBin: true - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - - 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'} - - indent-string@5.0.0: - resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} - engines: {node: '>=12'} - - 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==} - - ink-testing-library@3.0.0: - resolution: {integrity: sha512-ItyyoOmcm6yftb7c5mZI2HU22BWzue8PBbO3DStmY8B9xaqfKr7QJONiWOXcwVsOk/6HuVQ0v7N5xhPaR3jycA==} - engines: {node: '>=14.16'} - peerDependencies: - '@types/react': '>=18.0.0' - peerDependenciesMeta: - '@types/react': - optional: true - - ink@5.2.0: - resolution: {integrity: sha512-gHzSBBvsh/1ZYuGi+aKzU7RwnYIr6PSz56or9T90i4DDS99euhN7nYKOMR3OTev0dKIB6Zod3vSapYzqoilQcg==} - engines: {node: '>=18'} - peerDependencies: - '@types/react': '>=18.0.0' - react: '>=18.0.0' - react-devtools-core: ^4.19.1 - peerDependenciesMeta: - '@types/react': - optional: true - react-devtools-core: - optional: true - - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - - 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-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-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - - 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-fullwidth-code-point@4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} - - is-fullwidth-code-point@5.0.0: - resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} - engines: {node: '>=18'} - - is-generator-function@1.1.0: - resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} - engines: {node: '>= 0.4'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-in-ci@1.0.0: - resolution: {integrity: sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==} - engines: {node: '>=18'} - hasBin: true - - is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true - - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - 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-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - - 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-unicode-supported@2.1.0: - resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} - engines: {node: '>=18'} - - 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'} - - is-wsl@3.1.0: - resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} - engines: {node: '>=16'} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - isexe@3.1.1: - resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} - engines: {node: '>=16'} - - iterator.prototype@1.1.5: - resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} - engines: {node: '>= 0.4'} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - 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 - - jsx-ast-utils@3.3.5: - resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} - engines: {node: '>=4.0'} - - 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'} - - lint-staged@15.5.1: - resolution: {integrity: sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==} - engines: {node: '>=18.12.0'} - hasBin: true - - listr2@8.3.2: - resolution: {integrity: sha512-vsBzcU4oE+v0lj4FhVLzr9dBTv4/fHIa57l+GCwovP8MoFNZJTOhGU8PXd4v2VJCbECAaijBiHntiekFMLvo0g==} - engines: {node: '>=18.0.0'} - - 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==} - - log-update@6.1.0: - resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} - engines: {node: '>=18'} - - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - - loupe@3.1.3: - resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} - - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - - marked-terminal@7.3.0: - resolution: {integrity: sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==} - engines: {node: '>=16.0.0'} - peerDependencies: - marked: '>=1 <16' - - marked@11.2.0: - resolution: {integrity: sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==} - engines: {node: '>= 18'} - hasBin: true - - marked@15.0.8: - resolution: {integrity: sha512-rli4l2LyZqpQuRve5C0rkn6pj3hT8EWPC+zkAxFTAJLxRbENfTAhEQq9itrmf1Y81QtAX5D/MYlGlIomNgj9lA==} - engines: {node: '>= 18'} - hasBin: true - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - - meow@13.2.0: - resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} - engines: {node: '>=18'} - - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - - 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'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - - mimic-function@5.0.1: - resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} - engines: {node: '>=18'} - - 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==} - - 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 - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - - node-emoji@2.2.0: - resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} - engines: {node: '>=18'} - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - 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.entries@1.1.9: - resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} - 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'} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - - onetime@7.0.0: - resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} - engines: {node: '>=18'} - - open@10.1.1: - resolution: {integrity: sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==} - engines: {node: '>=18'} - - openai@4.95.1: - resolution: {integrity: sha512-IqJy+ymeW+k/Wq+2YVN3693OQMMcODRtHEYOlz263MdUwnN/Dwdl9c2EXSxLLtGEHkSHAfvzpDMHI5MaWJKXjQ==} - hasBin: true - peerDependencies: - ws: ^8.18.0 - zod: ^3.23.8 - peerDependenciesMeta: - ws: - optional: true - zod: - optional: true - - 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-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - package-manager-detector@1.2.0: - resolution: {integrity: sha512-PutJepsOtsqVfUsxCzgTTpyXmiAgvKptIgY4th5eq5UXXFhj5PxfQ9hnGkypMeovpAvVshFRItoFHYO18TCOqA==} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse5-htmlparser2-tree-adapter@6.0.1: - resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} - - parse5@5.1.1: - resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} - - parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - patch-console@2.0.0: - resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - 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-to-regexp@8.2.0: - resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} - engines: {node: '>=16'} - - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} - engines: {node: '>= 14.16'} - - peek-readable@7.0.0: - resolution: {integrity: sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==} - engines: {node: '>=18'} - - 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.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - - pidtree@0.6.0: - resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} - engines: {node: '>=0.10'} - hasBin: true - - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - - postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} - 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 - prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} - engines: {node: '>=0.6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} - engines: {node: '>= 0.8'} - - react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - - react-reconciler@0.29.2: - resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==} - engines: {node: '>=0.10.0'} - peerDependencies: - react: ^18.3.1 - - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} - engines: {node: '>=0.10.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'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} - engines: {node: '>= 0.4'} - hasBin: true - - resolve@2.0.0-next.5: - resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} - hasBin: true - - restore-cursor@4.0.0: - resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - restore-cursor@5.1.0: - resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} - engines: {node: '>=18'} - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rfdc@1.4.1: - resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - rollup@4.40.0: - resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - - run-applescript@7.0.0: - resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} - engines: {node: '>=18'} - - 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-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - 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'} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} - hasBin: true - - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} - engines: {node: '>= 18'} - - serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} - engines: {node: '>= 18'} - - 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'} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - 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'} - - shell-quote@1.8.2: - resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} - engines: {node: '>= 0.4'} - - 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@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - skin-tone@2.0.0: - resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} - engines: {node: '>=8'} - - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - slice-ansi@5.0.0: - resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} - engines: {node: '>=12'} - - slice-ansi@7.1.0: - resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} - engines: {node: '>=18'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - - std-env@3.9.0: - resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} - - string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} - - string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} - engines: {node: '>= 0.4'} - - string.prototype.repeat@1.0.0: - resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} - - 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.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - 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'} - - strtok3@10.2.2: - resolution: {integrity: sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==} - engines: {node: '>=18'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-hyperlinks@3.2.0: - resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} - engines: {node: '>=14.18'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - - 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.13: - resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} - engines: {node: '>=12.0.0'} - - tinypool@1.0.2: - resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} - engines: {node: '>=14.0.0'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - to-rotated@1.0.0: - resolution: {integrity: sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q==} - engines: {node: '>=18'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - - token-types@6.0.0: - resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==} - engines: {node: '>=14.16'} - - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} - - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - - 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==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - - type-fest@4.40.0: - resolution: {integrity: sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==} - engines: {node: '>=16'} - - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} - - 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.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} - engines: {node: '>=14.17'} - hasBin: true - - uint8array-extras@1.4.0: - resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} - engines: {node: '>=18'} - - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - - unicode-emoji-modifier-base@1.0.0: - resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} - engines: {node: '>=4'} - - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - use-interval@1.4.0: - resolution: {integrity: sha512-1betIJun2rXKLxa30AFOBZCeZhsBJoJ/3+gkCeYbJ63lAR//EnAb1NjNeFqzgqeM7zQfR76rrCUaA8DvfgoOpA==} - engines: {node: '>=8', npm: '>=5'} - peerDependencies: - react: '>=16.8.0 || ^17' - - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - - vite-node@3.1.2: - resolution: {integrity: sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - - vite@6.3.4: - resolution: {integrity: sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@3.1.2: - resolution: {integrity: sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.1.2 - '@vitest/ui': 3.1.2 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - web-streams-polyfill@4.0.0-beta.3: - resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} - engines: {node: '>= 14'} - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - - whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - - 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 - - which@5.0.0: - resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} - engines: {node: ^18.17.0 || >=20.5.0} - hasBin: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - widest-line@5.0.0: - resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} - engines: {node: '>=18'} - - 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@9.0.0: - resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} - engines: {node: '>=18'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - ws@8.18.1: - resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yaml@2.7.1: - resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} - engines: {node: '>= 14'} - hasBin: true - - yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - - yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - - 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'} - - yoga-layout@3.2.1: - resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} - - zod@3.24.3: - resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==} - snapshots: - '@alcalzone/ansi-tokenize@0.1.3': - dependencies: - ansi-styles: 6.2.1 - is-fullwidth-code-point: 4.0.0 - - '@colors/colors@1.5.0': - optional: true - - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - - '@esbuild/aix-ppc64@0.25.2': - optional: true - - '@esbuild/android-arm64@0.25.2': - optional: true - - '@esbuild/android-arm@0.25.2': - optional: true - - '@esbuild/android-x64@0.25.2': - optional: true - - '@esbuild/darwin-arm64@0.25.2': - optional: true - - '@esbuild/darwin-x64@0.25.2': - optional: true - - '@esbuild/freebsd-arm64@0.25.2': - optional: true - - '@esbuild/freebsd-x64@0.25.2': - optional: true - - '@esbuild/linux-arm64@0.25.2': - optional: true - - '@esbuild/linux-arm@0.25.2': - optional: true - - '@esbuild/linux-ia32@0.25.2': - optional: true - - '@esbuild/linux-loong64@0.25.2': - optional: true - - '@esbuild/linux-mips64el@0.25.2': - optional: true - - '@esbuild/linux-ppc64@0.25.2': - optional: true - - '@esbuild/linux-riscv64@0.25.2': - optional: true - - '@esbuild/linux-s390x@0.25.2': - optional: true - - '@esbuild/linux-x64@0.25.2': - optional: true - - '@esbuild/netbsd-arm64@0.25.2': - optional: true - - '@esbuild/netbsd-x64@0.25.2': - optional: true - - '@esbuild/openbsd-arm64@0.25.2': - optional: true - - '@esbuild/openbsd-x64@0.25.2': - optional: true - - '@esbuild/sunos-x64@0.25.2': - optional: true - - '@esbuild/win32-arm64@0.25.2': - optional: true - - '@esbuild/win32-ia32@0.25.2': - optional: true - - '@esbuild/win32-x64@0.25.2': - optional: true - - '@eslint-community/eslint-utils@4.6.1(eslint@8.57.1)': - dependencies: - eslint: 8.57.1 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.1': {} - - '@eslint/eslintrc@2.1.4': - dependencies: - ajv: 6.12.6 - debug: 4.4.0 - espree: 9.6.1 - globals: 13.24.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@8.57.1': {} - - '@eslint/js@9.24.0': {} - - '@humanwhocodes/config-array@0.13.0': - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/object-schema@2.0.3': {} - - '@inkjs/ui@2.0.0(ink@5.2.0(@types/react@18.3.20)(react@18.3.1))': - dependencies: - chalk: 5.4.1 - cli-spinners: 3.2.0 - deepmerge: 4.3.1 - figures: 6.1.0 - ink: 5.2.0(@types/react@18.3.20)(react@18.3.1) - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - - '@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 - - '@rollup/rollup-android-arm-eabi@4.40.0': - optional: true - - '@rollup/rollup-android-arm64@4.40.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.40.0': - optional: true - - '@rollup/rollup-darwin-x64@4.40.0': - optional: true - - '@rollup/rollup-freebsd-arm64@4.40.0': - optional: true - - '@rollup/rollup-freebsd-x64@4.40.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.40.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.40.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.40.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.40.0': - optional: true - - '@rollup/rollup-linux-loongarch64-gnu@4.40.0': - optional: true - - '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.40.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.40.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.40.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.40.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.40.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.40.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.40.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.40.0': - optional: true - - '@rtsao/scc@1.1.0': {} - - '@sindresorhus/is@4.6.0': {} - - '@tokenizer/inflate@0.2.7': - dependencies: - debug: 4.4.0 - fflate: 0.8.2 - token-types: 6.0.0 - transitivePeerDependencies: - - supports-color - - '@tokenizer/token@0.3.0': {} - - '@tsconfig/node10@1.0.11': {} - - '@tsconfig/node12@1.0.11': {} - - '@tsconfig/node14@1.0.3': {} - - '@tsconfig/node16@1.0.4': {} - - '@types/body-parser@1.19.5': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 22.14.1 - - '@types/cardinal@2.1.1': {} - - '@types/connect@3.4.38': - dependencies: - '@types/node': 22.14.1 - - '@types/diff@7.0.2': {} - - '@types/estree@1.0.7': {} - - '@types/express-serve-static-core@5.0.6': - dependencies: - '@types/node': 22.14.1 - '@types/qs': 6.9.18 - '@types/range-parser': 1.2.7 - '@types/send': 0.17.4 - - '@types/express@5.0.1': - dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 5.0.6 - '@types/serve-static': 1.15.7 - - '@types/http-errors@2.0.4': {} - - '@types/js-yaml@4.0.9': {} - - '@types/json5@0.0.29': {} - - '@types/marked-terminal@6.1.1': - dependencies: - '@types/cardinal': 2.1.1 - '@types/node': 22.14.1 - chalk: 5.4.1 - marked: 11.2.0 - - '@types/mime@1.3.5': {} - - '@types/node-fetch@2.6.12': - dependencies: - '@types/node': 22.14.1 - form-data: 4.0.2 - - '@types/node@18.19.86': - dependencies: - undici-types: 5.26.5 - - '@types/node@22.14.1': - dependencies: - undici-types: 6.21.0 - - '@types/prop-types@15.7.14': {} - - '@types/qs@6.9.18': {} - - '@types/range-parser@1.2.7': {} - - '@types/react@18.3.20': - dependencies: - '@types/prop-types': 15.7.14 - csstype: 3.1.3 - - '@types/semver@7.7.0': {} - - '@types/send@0.17.4': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 22.14.1 - - '@types/serve-static@1.15.7': - dependencies: - '@types/http-errors': 2.0.4 - '@types/node': 22.14.1 - '@types/send': 0.17.4 - - '@types/shell-quote@1.7.5': {} - - '@types/which@3.0.4': {} - - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - ts-api-utils: 1.4.3(typescript@5.8.3) - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3)': - dependencies: - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.0 - eslint: 8.57.1 - optionalDependencies: - typescript: 5.8.3 - 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@8.57.1)(typescript@5.8.3)': - dependencies: - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.0 - eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.8.3) - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@7.18.0': {} - - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.8.3)': - dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.0 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.1 - ts-api-utils: 1.4.3(typescript@5.8.3) - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.8.3)': - dependencies: - '@eslint-community/eslint-utils': 4.6.1(eslint@8.57.1) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) - eslint: 8.57.1 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/visitor-keys@7.18.0': - dependencies: - '@typescript-eslint/types': 7.18.0 - eslint-visitor-keys: 3.4.3 - - '@ungap/structured-clone@1.3.0': {} - - '@vitest/expect@3.1.2': - dependencies: - '@vitest/spy': 3.1.2 - '@vitest/utils': 3.1.2 - chai: 5.2.0 - tinyrainbow: 2.0.0 - - '@vitest/mocker@3.1.2(vite@6.3.4(@types/node@22.14.1)(yaml@2.7.1))': - dependencies: - '@vitest/spy': 3.1.2 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 6.3.4(@types/node@22.14.1)(yaml@2.7.1) - - '@vitest/pretty-format@3.1.2': - dependencies: - tinyrainbow: 2.0.0 - - '@vitest/runner@3.1.2': - dependencies: - '@vitest/utils': 3.1.2 - pathe: 2.0.3 - - '@vitest/snapshot@3.1.2': - dependencies: - '@vitest/pretty-format': 3.1.2 - magic-string: 0.30.17 - pathe: 2.0.3 - - '@vitest/spy@3.1.2': - dependencies: - tinyspy: 3.0.2 - - '@vitest/utils@3.1.2': - dependencies: - '@vitest/pretty-format': 3.1.2 - loupe: 3.1.3 - tinyrainbow: 2.0.0 - - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - - accepts@2.0.0: - dependencies: - mime-types: 3.0.1 - negotiator: 1.0.0 - - acorn-jsx@5.3.2(acorn@8.14.1): - dependencies: - acorn: 8.14.1 - - acorn-walk@8.3.4: - dependencies: - acorn: 8.14.1 - - acorn@8.14.1: {} - - agent-base@7.1.3: {} - - agentkeepalive@4.6.0: - dependencies: - humanize-ms: 1.2.1 - - 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-align@3.0.1: - dependencies: - string-width: 4.2.3 - - ansi-escapes@7.0.0: - dependencies: - environment: 1.1.0 - - ansi-regex@5.0.1: {} - - ansi-regex@6.1.0: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@6.2.1: {} - - any-promise@1.3.0: {} - - arg@4.1.3: {} - - 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.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - - array-union@2.1.0: {} - - array.prototype.findlast@1.2.5: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 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.23.9 - 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.23.9 - 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.23.9 - es-shim-unscopables: 1.1.0 - - array.prototype.tosorted@1.1.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.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.23.9 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - assertion-error@2.0.1: {} - - async-function@1.0.0: {} - - asynckit@0.4.0: {} - - auto-bind@5.0.1: {} - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - - balanced-match@1.0.2: {} - - body-parser@2.2.0: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.0 - http-errors: 2.0.0 - iconv-lite: 0.6.3 - on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.0 - type-is: 2.0.1 - transitivePeerDependencies: - - supports-color - - boxen@8.0.1: - dependencies: - ansi-align: 3.0.1 - camelcase: 8.0.0 - chalk: 5.4.1 - cli-boxes: 3.0.0 - string-width: 7.2.0 - type-fest: 4.40.0 - widest-line: 5.0.0 - wrap-ansi: 9.0.0 - - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.1: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - bundle-name@4.1.0: - dependencies: - run-applescript: 7.0.0 - - bytes@3.1.2: {} - - 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@8.0.0: {} - - chai@5.2.0: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.1.3 - pathval: 2.0.0 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - chalk@5.4.1: {} - - char-regex@1.0.2: {} - - check-error@2.1.1: {} - - cli-boxes@3.0.0: {} - - cli-cursor@4.0.0: - dependencies: - restore-cursor: 4.0.0 - - cli-cursor@5.0.0: - dependencies: - restore-cursor: 5.1.0 - - cli-highlight@2.1.11: - dependencies: - chalk: 4.1.2 - highlight.js: 10.7.3 - mz: 2.7.0 - parse5: 5.1.1 - parse5-htmlparser2-tree-adapter: 6.0.1 - yargs: 16.2.0 - - cli-spinners@3.2.0: {} - - cli-table3@0.6.5: - dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 - - cli-truncate@4.0.0: - dependencies: - slice-ansi: 5.0.0 - string-width: 7.2.0 - - cliui@7.0.4: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - code-excerpt@4.0.0: - dependencies: - convert-to-spaces: 2.0.1 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - colorette@2.0.20: {} - - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - - commander@13.1.0: {} - - concat-map@0.0.1: {} - - content-disposition@1.0.0: - dependencies: - safe-buffer: 5.2.1 - - content-type@1.0.5: {} - - convert-to-spaces@2.0.1: {} - - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - - create-require@1.1.1: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - csstype@3.1.3: {} - - 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.0: - dependencies: - ms: 2.1.3 - - deep-eql@5.0.2: {} - - deep-is@0.1.4: {} - - deepmerge@4.3.1: {} - - default-browser-id@5.0.0: {} - - default-browser@5.2.1: - dependencies: - bundle-name: 4.1.0 - default-browser-id: 5.0.0 - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-lazy-prop@3.0.0: {} - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - - delayed-stream@1.0.0: {} - - depd@2.0.0: {} - - diff@4.0.2: {} - - diff@7.0.0: {} - - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - - dotenv@16.5.0: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - ee-first@1.1.1: {} - - emoji-regex@10.4.0: {} - - emoji-regex@8.0.0: {} - - emojilib@2.4.0: {} - - encodeurl@2.0.0: {} - - environment@1.1.0: {} - - es-abstract@1.23.9: - 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-regex: 1.2.1 - 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 - 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-iterator-helpers@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-set-tostringtag: 2.1.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.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 - internal-slot: 1.1.0 - iterator.prototype: 1.1.5 - safe-array-concat: 1.1.3 - - es-module-lexer@1.6.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 - - es-toolkit@1.35.0: {} - - esbuild@0.25.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.2 - '@esbuild/android-arm': 0.25.2 - '@esbuild/android-arm64': 0.25.2 - '@esbuild/android-x64': 0.25.2 - '@esbuild/darwin-arm64': 0.25.2 - '@esbuild/darwin-x64': 0.25.2 - '@esbuild/freebsd-arm64': 0.25.2 - '@esbuild/freebsd-x64': 0.25.2 - '@esbuild/linux-arm': 0.25.2 - '@esbuild/linux-arm64': 0.25.2 - '@esbuild/linux-ia32': 0.25.2 - '@esbuild/linux-loong64': 0.25.2 - '@esbuild/linux-mips64el': 0.25.2 - '@esbuild/linux-ppc64': 0.25.2 - '@esbuild/linux-riscv64': 0.25.2 - '@esbuild/linux-s390x': 0.25.2 - '@esbuild/linux-x64': 0.25.2 - '@esbuild/netbsd-arm64': 0.25.2 - '@esbuild/netbsd-x64': 0.25.2 - '@esbuild/openbsd-arm64': 0.25.2 - '@esbuild/openbsd-x64': 0.25.2 - '@esbuild/sunos-x64': 0.25.2 - '@esbuild/win32-arm64': 0.25.2 - '@esbuild/win32-ia32': 0.25.2 - '@esbuild/win32-x64': 0.25.2 - - escalade@3.2.0: {} - - escape-html@1.0.3: {} - - escape-string-regexp@2.0.0: {} - - escape-string-regexp@4.0.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-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.8.3) - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.8 - 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: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) - 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.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.8.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - - eslint-plugin-react-refresh@0.4.19(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - - eslint-plugin-react@7.37.5(eslint@8.57.1): - dependencies: - array-includes: 3.1.8 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.1 - eslint: 8.57.1 - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 7.7.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint@8.57.1: - dependencies: - '@eslint-community/eslint-utils': 4.6.1(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.1 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.0 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - - espree@9.6.1: - dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) - eslint-visitor-keys: 3.4.3 - - 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.7 - - esutils@2.0.3: {} - - etag@1.8.1: {} - - event-target-shim@5.0.1: {} - - eventemitter3@5.0.1: {} - - 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 - - expect-type@1.2.1: {} - - express@5.1.0: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.0 - content-disposition: 1.0.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.0 - fresh: 2.0.0 - http-errors: 2.0.0 - merge-descriptors: 2.0.0 - mime-types: 3.0.1 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.0 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 - statuses: 2.0.1 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - 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: {} - - fast-npm-meta@0.4.2: {} - - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - - fdir@6.4.4(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 - - fflate@0.8.2: {} - - figures@6.1.0: - dependencies: - is-unicode-supported: 2.1.0 - - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.2.0 - - file-type@20.4.1: - dependencies: - '@tokenizer/inflate': 0.2.7 - strtok3: 10.2.2 - token-types: 6.0.0 - uint8array-extras: 1.4.0 - transitivePeerDependencies: - - supports-color - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - finalhandler@2.1.0: - dependencies: - debug: 4.4.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@3.2.0: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - rimraf: 3.0.2 - - flatted@3.3.3: {} - - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - - form-data-encoder@1.7.2: {} - - form-data@4.0.2: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - mime-types: 2.1.35 - - formdata-node@4.4.1: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 4.0.0-beta.3 - - forwarded@0.2.0: {} - - fresh@2.0.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: {} - - get-caller-file@2.0.5: {} - - get-east-asian-width@1.3.0: {} - - 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 - - git-cliff-darwin-arm64@2.8.0: - optional: true - - git-cliff-darwin-x64@2.8.0: - optional: true - - git-cliff-linux-arm64@2.8.0: - optional: true - - git-cliff-linux-x64@2.8.0: - optional: true - - git-cliff-windows-arm64@2.8.0: - optional: true - - git-cliff-windows-x64@2.8.0: - optional: true - - git-cliff@2.8.0: - dependencies: - execa: 8.0.1 - optionalDependencies: - git-cliff-darwin-arm64: 2.8.0 - git-cliff-darwin-x64: 2.8.0 - git-cliff-linux-arm64: 2.8.0 - git-cliff-linux-x64: 2.8.0 - git-cliff-windows-arm64: 2.8.0 - git-cliff-windows-x64: 2.8.0 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - 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@13.24.0: - dependencies: - type-fest: 0.20.2 - - 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 - - highlight.js@10.7.3: {} - - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.3 - debug: 4.4.0 - transitivePeerDependencies: - - supports-color - - human-signals@5.0.0: {} - - humanize-ms@1.2.1: - dependencies: - ms: 2.1.3 - - husky@9.1.7: {} - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - ieee754@1.2.1: {} - - ignore@5.3.2: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - - indent-string@5.0.0: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - ink-testing-library@3.0.0(@types/react@18.3.20): - optionalDependencies: - '@types/react': 18.3.20 - - ink@5.2.0(@types/react@18.3.20)(react@18.3.1): - dependencies: - '@alcalzone/ansi-tokenize': 0.1.3 - ansi-escapes: 7.0.0 - ansi-styles: 6.2.1 - auto-bind: 5.0.1 - chalk: 5.4.1 - cli-boxes: 3.0.0 - cli-cursor: 4.0.0 - cli-truncate: 4.0.0 - code-excerpt: 4.0.0 - es-toolkit: 1.35.0 - indent-string: 5.0.0 - is-in-ci: 1.0.0 - patch-console: 2.0.0 - react: 18.3.1 - react-reconciler: 0.29.2(react@18.3.1) - scheduler: 0.23.2 - signal-exit: 3.0.7 - slice-ansi: 7.1.0 - stack-utils: 2.0.6 - string-width: 7.2.0 - type-fest: 4.40.0 - widest-line: 5.0.0 - wrap-ansi: 9.0.0 - ws: 8.18.1 - yoga-layout: 3.2.1 - optionalDependencies: - '@types/react': 18.3.20 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - - ipaddr.js@1.9.1: {} - - 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-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-docker@3.0.0: {} - - is-extglob@2.1.1: {} - - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-fullwidth-code-point@3.0.0: {} - - is-fullwidth-code-point@4.0.0: {} - - is-fullwidth-code-point@5.0.0: - dependencies: - get-east-asian-width: 1.3.0 - - is-generator-function@1.1.0: - dependencies: - call-bound: 1.0.4 - 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-in-ci@1.0.0: {} - - is-inside-container@1.0.0: - dependencies: - is-docker: 3.0.0 - - is-map@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-path-inside@3.0.3: {} - - is-promise@4.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-unicode-supported@2.1.0: {} - - 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 - - is-wsl@3.1.0: - dependencies: - is-inside-container: 1.0.0 - - isarray@2.0.5: {} - - isexe@2.0.0: {} - - isexe@3.1.1: {} - - iterator.prototype@1.1.5: - dependencies: - define-data-property: 1.1.4 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - has-symbols: 1.1.0 - set-function-name: 2.0.2 - - js-tokens@4.0.0: {} - - 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 - - jsx-ast-utils@3.3.5: - dependencies: - array-includes: 3.1.8 - array.prototype.flat: 1.3.3 - object.assign: 4.1.7 - object.values: 1.2.1 - - 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: {} - - lint-staged@15.5.1: - dependencies: - chalk: 5.4.1 - commander: 13.1.0 - debug: 4.4.0 - execa: 8.0.1 - lilconfig: 3.1.3 - listr2: 8.3.2 - micromatch: 4.0.8 - pidtree: 0.6.0 - string-argv: 0.3.2 - yaml: 2.7.1 - transitivePeerDependencies: - - supports-color - - listr2@8.3.2: - dependencies: - cli-truncate: 4.0.0 - colorette: 2.0.20 - eventemitter3: 5.0.1 - log-update: 6.1.0 - rfdc: 1.4.1 - wrap-ansi: 9.0.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - - log-update@6.1.0: - dependencies: - ansi-escapes: 7.0.0 - cli-cursor: 5.0.0 - slice-ansi: 7.1.0 - strip-ansi: 7.1.0 - wrap-ansi: 9.0.0 - - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - - loupe@3.1.3: {} - - magic-string@0.30.17: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - - make-error@1.3.6: {} - - marked-terminal@7.3.0(patch_hash=536fe9685e91d559cf29a033191aa39da45729949e9d1c69989255091c8618fb)(marked@15.0.8): - dependencies: - ansi-escapes: 7.0.0 - ansi-regex: 6.1.0 - chalk: 5.4.1 - cli-highlight: 2.1.11 - cli-table3: 0.6.5 - marked: 15.0.8 - node-emoji: 2.2.0 - supports-hyperlinks: 3.2.0 - - marked@11.2.0: {} - - marked@15.0.8: {} - - math-intrinsics@1.1.0: {} - - media-typer@1.1.0: {} - - meow@13.2.0: {} - - merge-descriptors@2.0.0: {} - - merge-stream@2.0.0: {} - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mime-db@1.52.0: {} - - mime-db@1.54.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mime-types@3.0.1: - dependencies: - mime-db: 1.54.0 - - mimic-fn@2.1.0: {} - - mimic-fn@4.0.0: {} - - mimic-function@5.0.1: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.11 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.1 - - minimist@1.2.8: {} - - 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: {} - - natural-compare@1.4.0: {} - - negotiator@1.0.0: {} - - node-domexception@1.0.0: {} - - node-emoji@2.2.0: - dependencies: - '@sindresorhus/is': 4.6.0 - char-regex: 1.0.2 - emojilib: 2.4.0 - skin-tone: 2.0.0 - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.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.entries@1.1.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - 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.23.9 - - 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 - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - onetime@6.0.0: - dependencies: - mimic-fn: 4.0.0 - - onetime@7.0.0: - dependencies: - mimic-function: 5.0.1 - - open@10.1.1: - dependencies: - default-browser: 5.2.1 - define-lazy-prop: 3.0.0 - is-inside-container: 1.0.0 - is-wsl: 3.1.0 - - openai@4.95.1(ws@8.18.1)(zod@3.24.3): - dependencies: - '@types/node': 18.19.86 - '@types/node-fetch': 2.6.12 - abort-controller: 3.0.0 - agentkeepalive: 4.6.0 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0 - optionalDependencies: - ws: 8.18.1 - zod: 3.24.3 - transitivePeerDependencies: - - encoding - - 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-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - package-manager-detector@1.2.0: {} - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse5-htmlparser2-tree-adapter@6.0.1: - dependencies: - parse5: 6.0.1 - - parse5@5.1.1: {} - - parse5@6.0.1: {} - - parseurl@1.3.3: {} - - patch-console@2.0.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-to-regexp@8.2.0: {} - - path-type@4.0.0: {} - - pathe@2.0.3: {} - - pathval@2.0.0: {} - - peek-readable@7.0.0: {} - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - picomatch@4.0.2: {} - - pidtree@0.6.0: {} - - possible-typed-array-names@1.1.0: {} - - postcss@8.5.3: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - prettier@3.5.3: {} - - prop-types@15.8.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - punycode@2.3.1: {} - - qs@6.14.0: - dependencies: - side-channel: 1.1.0 - - queue-microtask@1.2.3: {} - - range-parser@1.2.1: {} - - raw-body@3.0.0: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.6.3 - unpipe: 1.0.0 - - react-is@16.13.1: {} - - react-reconciler@0.29.2(react@18.3.1): - dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 - - react@18.3.1: - dependencies: - loose-envify: 1.4.0 - - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - 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-from@4.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 - - resolve@2.0.0-next.5: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - restore-cursor@4.0.0: - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - - restore-cursor@5.1.0: - dependencies: - onetime: 7.0.0 - signal-exit: 4.1.0 - - reusify@1.1.0: {} - - rfdc@1.4.1: {} - - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - - rollup@4.40.0: - dependencies: - '@types/estree': 1.0.7 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.40.0 - '@rollup/rollup-android-arm64': 4.40.0 - '@rollup/rollup-darwin-arm64': 4.40.0 - '@rollup/rollup-darwin-x64': 4.40.0 - '@rollup/rollup-freebsd-arm64': 4.40.0 - '@rollup/rollup-freebsd-x64': 4.40.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.40.0 - '@rollup/rollup-linux-arm-musleabihf': 4.40.0 - '@rollup/rollup-linux-arm64-gnu': 4.40.0 - '@rollup/rollup-linux-arm64-musl': 4.40.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.40.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0 - '@rollup/rollup-linux-riscv64-gnu': 4.40.0 - '@rollup/rollup-linux-riscv64-musl': 4.40.0 - '@rollup/rollup-linux-s390x-gnu': 4.40.0 - '@rollup/rollup-linux-x64-gnu': 4.40.0 - '@rollup/rollup-linux-x64-musl': 4.40.0 - '@rollup/rollup-win32-arm64-msvc': 4.40.0 - '@rollup/rollup-win32-ia32-msvc': 4.40.0 - '@rollup/rollup-win32-x64-msvc': 4.40.0 - fsevents: 2.3.3 - - router@2.2.0: - dependencies: - debug: 4.4.0 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.2.0 - transitivePeerDependencies: - - supports-color - - run-applescript@7.0.0: {} - - 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-buffer@5.2.1: {} - - 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 - - safer-buffer@2.1.2: {} - - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 - - semver@7.7.1: {} - - send@1.2.0: - dependencies: - debug: 4.4.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.0 - mime-types: 3.0.1 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - - serve-static@2.2.0: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.0 - transitivePeerDependencies: - - supports-color - - 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 - - setprototypeof@1.2.0: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - shell-quote@1.8.2: {} - - 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@3.0.7: {} - - signal-exit@4.1.0: {} - - skin-tone@2.0.0: - dependencies: - unicode-emoji-modifier-base: 1.0.0 - - slash@3.0.0: {} - - slice-ansi@5.0.0: - dependencies: - ansi-styles: 6.2.1 - is-fullwidth-code-point: 4.0.0 - - slice-ansi@7.1.0: - dependencies: - ansi-styles: 6.2.1 - is-fullwidth-code-point: 5.0.0 - - source-map-js@1.2.1: {} - - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 - - stackback@0.0.2: {} - - statuses@2.0.1: {} - - std-env@3.9.0: {} - - string-argv@0.3.2: {} - - 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@7.2.0: - dependencies: - emoji-regex: 10.4.0 - get-east-asian-width: 1.3.0 - strip-ansi: 7.1.0 - - string.prototype.matchall@4.0.12: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.4 - set-function-name: 2.0.2 - side-channel: 1.1.0 - - string.prototype.repeat@1.0.0: - dependencies: - define-properties: 1.2.1 - es-abstract: 1.23.9 - - 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.23.9 - 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.0: - dependencies: - ansi-regex: 6.1.0 - - strip-bom@3.0.0: {} - - strip-final-newline@3.0.0: {} - - strip-json-comments@3.1.1: {} - - strtok3@10.2.2: - dependencies: - '@tokenizer/token': 0.3.0 - peek-readable: 7.0.0 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-hyperlinks@3.2.0: - dependencies: - has-flag: 4.0.0 - supports-color: 7.2.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - text-table@0.2.0: {} - - 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.13: - dependencies: - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 - - tinypool@1.0.2: {} - - tinyrainbow@2.0.0: {} - - tinyspy@3.0.2: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - to-rotated@1.0.0: {} - - toidentifier@1.0.1: {} - - token-types@6.0.0: - dependencies: - '@tokenizer/token': 0.3.0 - ieee754: 1.2.1 - - tr46@0.0.3: {} - - tr46@5.1.1: - dependencies: - punycode: 2.3.1 - - ts-api-utils@1.4.3(typescript@5.8.3): - dependencies: - typescript: 5.8.3 - - ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3): - 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': 22.14.1 - acorn: 8.14.1 - 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.8.3 - 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 - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-fest@0.20.2: {} - - type-fest@4.40.0: {} - - type-is@2.0.1: - dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.1 - - 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.8.3: {} - - uint8array-extras@1.4.0: {} - - 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@5.26.5: {} - - undici-types@6.21.0: {} - - unicode-emoji-modifier-base@1.0.0: {} - - unpipe@1.0.0: {} - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - use-interval@1.4.0(react@18.3.1): - dependencies: - react: 18.3.1 - - v8-compile-cache-lib@3.0.1: {} - - vary@1.1.2: {} - - vite-node@3.1.2(@types/node@22.14.1)(yaml@2.7.1): - dependencies: - cac: 6.7.14 - debug: 4.4.0 - es-module-lexer: 1.6.0 - pathe: 2.0.3 - vite: 6.3.4(@types/node@22.14.1)(yaml@2.7.1) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite@6.3.4(@types/node@22.14.1)(yaml@2.7.1): - dependencies: - esbuild: 0.25.2 - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 - postcss: 8.5.3 - rollup: 4.40.0 - tinyglobby: 0.2.13 - optionalDependencies: - '@types/node': 22.14.1 - fsevents: 2.3.3 - yaml: 2.7.1 - - vitest@3.1.2(@types/node@22.14.1)(yaml@2.7.1): - dependencies: - '@vitest/expect': 3.1.2 - '@vitest/mocker': 3.1.2(vite@6.3.4(@types/node@22.14.1)(yaml@2.7.1)) - '@vitest/pretty-format': 3.1.2 - '@vitest/runner': 3.1.2 - '@vitest/snapshot': 3.1.2 - '@vitest/spy': 3.1.2 - '@vitest/utils': 3.1.2 - chai: 5.2.0 - debug: 4.4.0 - expect-type: 1.2.1 - magic-string: 0.30.17 - pathe: 2.0.3 - std-env: 3.9.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.13 - tinypool: 1.0.2 - tinyrainbow: 2.0.0 - vite: 6.3.4(@types/node@22.14.1)(yaml@2.7.1) - vite-node: 3.1.2(@types/node@22.14.1)(yaml@2.7.1) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 22.14.1 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - web-streams-polyfill@4.0.0-beta.3: {} - - webidl-conversions@3.0.1: {} - - webidl-conversions@7.0.0: {} - - whatwg-url@14.2.0: - dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - - 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.0 - 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 - - which@5.0.0: - dependencies: - isexe: 3.1.1 - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - widest-line@5.0.0: - dependencies: - string-width: 7.2.0 - - 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@9.0.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 7.2.0 - strip-ansi: 7.1.0 - - wrappy@1.0.2: {} - - ws@8.18.1: {} - - y18n@5.0.8: {} - - yaml@2.7.1: {} - - yargs-parser@20.2.9: {} - - yargs@16.2.0: - dependencies: - cliui: 7.0.4 - 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: 20.2.9 - - yn@3.1.1: {} - - yocto-queue@0.1.0: {} - - yoga-layout@3.2.1: {} - - zod@3.24.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index edb77fe235..1c38470ff5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,10 +1,5 @@ packages: - - codex-cli - docs - - packages/* ignoredBuiltDependencies: - esbuild - -patchedDependencies: - marked-terminal@7.3.0: patches/marked-terminal@7.3.0.patch diff --git a/scripts/publish_to_npm.py b/scripts/publish_to_npm.py new file mode 100755 index 0000000000..f79843ffc8 --- /dev/null +++ b/scripts/publish_to_npm.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +""" +Download a release artifact for the npm package and publish it. + +Given a release version like `0.20.0`, this script: + - Downloads the `codex-npm-.tgz` asset from the GitHub release + tagged `rust-v` in the `openai/codex` repository using `gh`. + - Runs `npm publish` on the downloaded tarball to publish `@openai/codex`. + +Flags: + - `--dry-run` delegates to `npm publish --dry-run`. The artifact is still + downloaded so npm can inspect the archive contents without publishing. + +Requirements: + - GitHub CLI (`gh`) must be installed and authenticated to access the repo. + - npm must be logged in with an account authorized to publish + `@openai/codex`. This may trigger a browser for 2FA. +""" + +import argparse +import os +import subprocess +import sys +import tempfile +from pathlib import Path + + +def run_checked(cmd: list[str], cwd: Path | None = None) -> None: + """Run a subprocess command and raise if it fails.""" + proc = subprocess.run(cmd, cwd=str(cwd) if cwd else None) + proc.check_returncode() + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Download the npm release artifact for a given version and publish it." + ) + ) + parser.add_argument( + "version", + help="Release version to publish, e.g. 0.20.0 (without the 'v' prefix)", + ) + parser.add_argument( + "--dir", + type=Path, + help=( + "Optional directory to download the artifact into. Defaults to a temporary directory." + ), + ) + parser.add_argument( + "-n", + "--dry-run", + action="store_true", + help="Delegate to `npm publish --dry-run` (still downloads the artifact).", + ) + args = parser.parse_args() + + version: str = args.version.lstrip("v") + tag = f"rust-v{version}" + asset_name = f"codex-npm-{version}.tgz" + + download_dir_context_manager = ( + tempfile.TemporaryDirectory() if args.dir is None else None + ) + # Use provided dir if set, else the temporary one created above + download_dir: Path = args.dir if args.dir else Path(download_dir_context_manager.name) # type: ignore[arg-type] + download_dir.mkdir(parents=True, exist_ok=True) + + # 1) Download the artifact using gh + repo = "openai/codex" + gh_cmd = [ + "gh", + "release", + "download", + tag, + "--repo", + repo, + "--pattern", + asset_name, + "--dir", + str(download_dir), + ] + print(f"Downloading {asset_name} from {repo}@{tag} into {download_dir}...") + # Even in --dry-run we download so npm can inspect the tarball. + run_checked(gh_cmd) + + artifact_path = download_dir / asset_name + if not args.dry_run and not artifact_path.is_file(): + print( + f"Error: expected artifact not found after download: {artifact_path}", + file=sys.stderr, + ) + return 1 + + # 2) Publish to npm + npm_cmd = ["npm", "publish"] + if args.dry_run: + npm_cmd.append("--dry-run") + npm_cmd.append(str(artifact_path)) + + # Ensure CI is unset so npm can open a browser for 2FA if needed. + env = os.environ.copy() + if env.get("CI"): + env.pop("CI") + + print("Running:", " ".join(npm_cmd)) + proc = subprocess.run(npm_cmd, env=env) + proc.check_returncode() + + print("Publish complete.") + # Keep the temporary directory alive until here; it is cleaned up on exit + return 0 + + +if __name__ == "__main__": + sys.exit(main())