diff --git a/.github/ISSUE_TEMPLATE/2-bug-report.yml b/.github/ISSUE_TEMPLATE/2-bug-report.yml
index 11476d5074..9db7f2ef2e 100644
--- a/.github/ISSUE_TEMPLATE/2-bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/2-bug-report.yml
@@ -26,6 +26,7 @@ body:
label: Which model were you using?
description: Like `gpt-4.1`, `o4-mini`, `o3`, etc.
- type: input
+ id: platform
attributes:
label: What platform is your computer?
description: |
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 51b84cc930..508b5b9bd5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,40 +19,56 @@ jobs:
with:
node-version: 22
- # Run codex-cli/ tasks first because they are higher signal.
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10.8.1
+ run_install: false
- - name: Install dependencies (codex-cli)
- working-directory: codex-cli
- run: npm ci
-
- - name: Check formatting (codex-cli)
- working-directory: codex-cli
- run: npm run format
-
- - name: Run tests (codex-cli)
- working-directory: codex-cli
- run: npm run test
-
- - name: Lint (codex-cli)
- working-directory: codex-cli
+ - name: Get pnpm store directory
+ id: pnpm-cache
+ shell: bash
run: |
- npm run lint -- \
+ 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
+
+ # 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 (codex-cli)
- working-directory: codex-cli
- run: npm run typecheck
+ - name: Type-check
+ run: pnpm run typecheck
- - name: Build (codex-cli)
- working-directory: codex-cli
- run: npm run build
+ - name: Build
+ run: pnpm run build
- # Run formatting checks in the root directory last.
-
- - name: Install dependencies (root)
- run: npm ci
-
- - name: Check formatting (root)
- run: npm run format
+ - name: Ensure README.md contains only ASCII and certain Unicode code points
+ run: ./scripts/asciicheck.py README.md
+ - name: Check README ToC
+ run: python3 scripts/readme_toc.py README.md
diff --git a/.gitignore b/.gitignore
index 09f3f04532..72326607ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,11 @@
# deps
+# Node.js dependencies
node_modules
+.pnpm-store
+.pnpm-debug.log
+
+# Keep pnpm-lock.yaml
+!pnpm-lock.yaml
# build
dist/
@@ -17,9 +23,14 @@ result
.vscode/
.idea/
.history/
+.zed/
*.swp
*~
+# cli tools
+CLAUDE.md
+.claude/
+
# caches
.cache/
.turbo/
@@ -61,9 +72,8 @@ Icon?
# Unwanted package managers
.yarn/
yarn.lock
-pnpm-lock.yaml
# release
package.json-e
session.ts-e
-CHANGELOG.ignore.md
\ No newline at end of file
+CHANGELOG.ignore.md
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000000..e02c24e2b5
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+pnpm lint-staged
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000000..4153fe87bb
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,4 @@
+shamefully-hoist=true
+strict-peer-dependencies=false
+node-linker=hoisted
+prefer-workspace-packages=true
diff --git a/.prettierignore b/.prettierignore
index 07700970c0..1800016bda 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,2 +1,3 @@
/codex-cli/dist
/codex-cli/node_modules
+pnpm-lock.yaml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 320082b327..a60f38ad30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,76 @@
You can install any of these versions: `npm install -g codex@version`
-## 0.1.2504172351
+## `0.1.2504221401`
+
+### 🚀 Features
+
+- Show actionable errors when api keys are missing (#523)
+- Add CLI `--version` flag (#492)
+
+### 🐛 Bug Fixes
+
+- Agent loop for ZDR (`disableResponseStorage`) (#543)
+- Fix relative `workdir` check for `apply_patch` (#556)
+- Minimal mid-stream #429 retry loop using existing back-off (#506)
+- Inconsistent usage of base URL and API key (#507)
+- Remove requirement for api key for ollama (#546)
+- Support `[provider]_BASE_URL` (#542)
+
+## `0.1.2504220136`
+
+### 🚀 Features
+
+- Add support for ZDR orgs (#481)
+- Include fractional portion of chunk that exceeds stdout/stderr limit (#497)
+
+## `0.1.2504211509`
+
+### 🚀 Features
+
+- Support multiple providers via Responses-Completion transformation (#247)
+- Add user-defined safe commands configuration and approval logic #380 (#386)
+- Allow switching approval modes when prompted to approve an edit/command (#400)
+- Add support for `/diff` command autocomplete in TerminalChatInput (#431)
+- Auto-open model selector if user selects deprecated model (#427)
+- Read approvalMode from config file (#298)
+- `/diff` command to view git diff (#426)
+- Tab completions for file paths (#279)
+- Add /command autocomplete (#317)
+- Allow multi-line input (#438)
+
+### 🐛 Bug Fixes
+
+- `full-auto` support in quiet mode (#374)
+- Enable shell option for child process execution (#391)
+- Configure husky and lint-staged for pnpm monorepo (#384)
+- Command pipe execution by improving shell detection (#437)
+- Name of the file not matching the name of the component (#354)
+- Allow proper exit from new Switch approval mode dialog (#453)
+- Ensure /clear resets context and exclude system messages from approximateTokenUsed count (#443)
+- `/clear` now clears terminal screen and resets context left indicator (#425)
+- Correct fish completion function name in CLI script (#485)
+- Auto-open model-selector when model is not found (#448)
+- Remove unnecessary isLoggingEnabled() checks (#420)
+- Improve test reliability for `raw-exec` (#434)
+- Unintended tear down of agent loop (#483)
+- Remove extraneous type casts (#462)
+
+## `0.1.2504181820`
+
+### 🚀 Features
+
+- Add `/bug` report command (#312)
+- Notify when a newer version is available (#333)
+
+### 🐛 Bug Fixes
+
+- Update context left display logic in TerminalChatInput component (#307)
+- Improper spawn of sh on Windows Powershell (#318)
+- `/bug` report command, thinking indicator (#381)
+- Include pnpm lock file (#377)
+
+## `0.1.2504172351`
### 🚀 Features
@@ -14,7 +83,7 @@ You can install any of these versions: `npm install -g codex@version`
- Raw-exec-process-group.test improve reliability and error handling (#280)
- Canonicalize the writeable paths used in seatbelt policy (#275)
-## 0.1.2504172304
+## `0.1.2504172304`
### 🚀 Features
diff --git a/PNPM.md b/PNPM.md
new file mode 100644
index 0000000000..860633c8e1
--- /dev/null
+++ b/PNPM.md
@@ -0,0 +1,70 @@
+# Migration to pnpm
+
+This project has been migrated from npm to pnpm to improve dependency management and developer experience.
+
+## Why pnpm?
+
+- **Faster installation**: pnpm is significantly faster than npm and yarn
+- **Disk space savings**: pnpm uses a content-addressable store to avoid duplication
+- **Phantom dependency prevention**: pnpm creates a strict node_modules structure
+- **Native workspaces support**: simplified monorepo management
+
+## How to use pnpm
+
+### Installation
+
+```bash
+# Global installation of pnpm
+npm install -g pnpm@10.8.1
+
+# Or with corepack (available with Node.js 22+)
+corepack enable
+corepack prepare pnpm@10.8.1 --activate
+```
+
+### Common commands
+
+| npm command | pnpm equivalent |
+| --------------- | ---------------- |
+| `npm install` | `pnpm install` |
+| `npm run build` | `pnpm run build` |
+| `npm test` | `pnpm test` |
+| `npm run lint` | `pnpm run lint` |
+
+### Workspace-specific commands
+
+| Action | Command |
+| ------------------------------------------ | ---------------------------------------- |
+| Run a command in a specific package | `pnpm --filter @openai/codex run build` |
+| Install a dependency in a specific package | `pnpm --filter @openai/codex add lodash` |
+| Run a command in all packages | `pnpm -r run test` |
+
+## Monorepo structure
+
+```
+codex/
+├── pnpm-workspace.yaml # Workspace configuration
+├── .npmrc # pnpm configuration
+├── package.json # Root dependencies and scripts
+├── codex-cli/ # Main package
+│ └── package.json # codex-cli specific dependencies
+└── docs/ # Documentation (future package)
+```
+
+## Configuration files
+
+- **pnpm-workspace.yaml**: Defines the packages included in the monorepo
+- **.npmrc**: Configures pnpm behavior
+- **Root package.json**: Contains shared scripts and dependencies
+
+## CI/CD
+
+CI/CD workflows have been updated to use pnpm instead of npm. Make sure your CI environments use pnpm 10.8.1 or higher.
+
+## Known issues
+
+If you encounter issues with pnpm, try the following solutions:
+
+1. Remove the `node_modules` folder and `pnpm-lock.yaml` file, then run `pnpm install`
+2. Make sure you're using pnpm 10.8.1 or higher
+3. Verify that Node.js 22 or higher is installed
diff --git a/README.md b/README.md
index e03fea202e..ab4701bf7d 100644
--- a/README.md
+++ b/README.md
@@ -10,24 +10,29 @@
Table of Contents
+
+
- [Experimental Technology Disclaimer](#experimental-technology-disclaimer)
- [Quickstart](#quickstart)
-- [Why Codex?](#whycodex)
-- [Security Model \& Permissions](#securitymodelpermissions)
+- [Why Codex?](#why-codex)
+- [Security Model & Permissions](#security-model--permissions)
- [Platform sandboxing details](#platform-sandboxing-details)
-- [System Requirements](#systemrequirements)
-- [CLI Reference](#clireference)
-- [Memory \& Project Docs](#memoryprojectdocs)
-- [Non‑interactive / CI mode](#noninteractivecimode)
+- [System Requirements](#system-requirements)
+- [CLI Reference](#cli-reference)
+- [Memory & Project Docs](#memory--project-docs)
+- [Non-interactive / CI mode](#non-interactive--ci-mode)
+- [Tracing / Verbose Logging](#tracing--verbose-logging)
- [Recipes](#recipes)
- [Installation](#installation)
- [Configuration](#configuration)
- [FAQ](#faq)
+- [Zero Data Retention (ZDR) Organization Limitation](#zero-data-retention-zdr-organization-limitation)
- [Funding Opportunity](#funding-opportunity)
- [Contributing](#contributing)
- [Development workflow](#development-workflow)
- - [Nix Flake Development](#nix-flake-development)
- - [Writing high‑impact code changes](#writing-highimpact-code-changes)
+ - [Git Hooks with Husky](#git-hooks-with-husky)
+ - [Debugging](#debugging)
+ - [Writing high-impact code changes](#writing-high-impact-code-changes)
- [Opening a pull request](#opening-a-pull-request)
- [Review process](#review-process)
- [Community values](#community-values)
@@ -35,9 +40,12 @@
- [Contributor License Agreement (CLA)](#contributor-license-agreement-cla)
- [Quick fixes](#quick-fixes)
- [Releasing `codex`](#releasing-codex)
-- [Security \& Responsible AI](#securityresponsibleai)
+ - [Alternative Build Options](#alternative-build-options)
+ - [Nix Flake Development](#nix-flake-development)
+- [Security & Responsible AI](#security--responsible-ai)
- [License](#license)
-- [Zero Data Retention (ZDR) Organization Limitation](#zero-data-retention-zdr-organization-limitation)
+
+
Use
+
+> Codex also allows you to use other providers that support the OpenAI Chat Completions API. You can set the provider in the config file or use the `--provider` flag. The possible options for `--provider` are:
+>
+> - openai (default)
+> - openrouter
+> - gemini
+> - ollama
+> - mistral
+> - deepseek
+> - xai
+> - groq
+>
+> If you use a provider other than OpenAI, you will need to set the API key for the provider in the config file or in the environment variable as:
+>
+> ```shell
+> export --provider to use other models
+
Run interactively:
```shell
@@ -94,59 +123,59 @@ codex "explain this codebase to me"
codex --approval-mode full-auto "create the fanciest todo-list app"
```
-That’s it – Codex will scaffold a file, run it inside a sandbox, install any
+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.
+they'll be committed to your working directory.
---
-## Why Codex?
+## 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
+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!
+- **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 ✨
+- **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
+## Security Model & Permissions
Codex lets you decide _how much autonomy_ the agent receives and auto-approval policy via the
`--approval-mode` flag (or the interactive onboarding prompt):
-| Mode | What the agent may do without asking | Still requires approval |
-| ------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
-| **Suggest**
(default) | • Read any file in the repo | • **All** file writes/patches
• **Any** arbitrary shell commands (aside from reading files) |
-| **Auto Edit** | • Read **and** apply‑patch writes to files | • **All** shell commands |
-| **Full Auto** | • Read/write files
• Execute shell commands (network disabled, writes limited to your workdir) | – |
+| Mode | What the agent may do without asking | Still requires approval |
+| ------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
+| **Suggest**
(default) |
;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
- // present* (2,4,6,8) as newline; plain (1) as submit.
+ // 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.
- // Xterm encodes modifier keys in `mod` – bit‑2 (value 4) indicates
- // that Ctrl was held. We avoid the `&` bitwise operator (disallowed
- // by our ESLint config) by using arithmetic instead.
const hasCtrl = Math.floor(mod / 4) % 2 === 1;
if (hasCtrl) {
if (onSubmit) {
onSubmit(buffer.current.getText());
}
} else {
- // Any variant without Ctrl just inserts newline (Shift, Alt, none)
+ 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);
@@ -350,6 +374,16 @@ const MultilineTextEditorInner = (
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);
+ },
}),
[],
);
diff --git a/codex-cli/src/components/chat/terminal-chat-command-review.tsx b/codex-cli/src/components/chat/terminal-chat-command-review.tsx
index 124178b0e1..912af97961 100644
--- a/codex-cli/src/components/chat/terminal-chat-command-review.tsx
+++ b/codex-cli/src/components/chat/terminal-chat-command-review.tsx
@@ -15,11 +15,18 @@ const DEFAULT_DENY_MESSAGE =
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",
@@ -70,6 +77,7 @@ export function TerminalChatCommandReview({
const opts: Array<
| { label: string; value: ReviewDecision }
| { label: string; value: "edit" }
+ | { label: string; value: "switch" }
> = [
{
label: "Yes (y)",
@@ -93,6 +101,11 @@ export function TerminalChatCommandReview({
label: "Edit or give feedback (e)",
value: "edit",
},
+ // allow switching approval mode
+ {
+ label: "Switch approval mode (s)",
+ value: "switch",
+ },
{
label: "No, and keep going (n)",
value: ReviewDecision.NO_CONTINUE,
@@ -106,44 +119,50 @@ export function TerminalChatCommandReview({
return opts;
}, [showAlwaysApprove]);
- useInput((input, key) => {
- if (mode === "select") {
- if (input === "y") {
- onReviewCommand(ReviewDecision.YES);
- } else if (input === "x") {
- onReviewCommand(ReviewDecision.EXPLAIN);
- } else if (input === "e") {
- setMode("input");
- } else if (input === "n") {
- onReviewCommand(
- ReviewDecision.NO_CONTINUE,
- "Don't do that, keep going though",
- );
- } else if (input === "a" && showAlwaysApprove) {
- onReviewCommand(ReviewDecision.ALWAYS);
- } else if (key.escape) {
- onReviewCommand(ReviewDecision.NO_EXIT);
+ useInput(
+ (input, key) => {
+ if (mode === "select") {
+ if (input === "y") {
+ onReviewCommand(ReviewDecision.YES);
+ } else if (input === "x") {
+ onReviewCommand(ReviewDecision.EXPLAIN);
+ } else if (input === "e") {
+ setMode("input");
+ } else if (input === "n") {
+ onReviewCommand(
+ ReviewDecision.NO_CONTINUE,
+ "Don't do that, keep going though",
+ );
+ } else if (input === "a" && showAlwaysApprove) {
+ onReviewCommand(ReviewDecision.ALWAYS);
+ } else if (input === "s") {
+ // switch approval mode
+ onSwitchApprovalMode();
+ } else if (key.escape) {
+ onReviewCommand(ReviewDecision.NO_EXIT);
+ }
+ } else if (mode === "explanation") {
+ // When in explanation mode, any key returns to select mode
+ if (key.return || key.escape || input === "x") {
+ setMode("select");
+ }
+ } else {
+ // text entry mode
+ if (key.return) {
+ // if user hit enter on empty msg, fall back to DEFAULT_DENY_MESSAGE
+ const custom = msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg;
+ onReviewCommand(ReviewDecision.NO_CONTINUE, custom);
+ } else if (key.escape) {
+ // treat escape as denial with default message as well
+ onReviewCommand(
+ ReviewDecision.NO_CONTINUE,
+ msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg,
+ );
+ }
}
- } else if (mode === "explanation") {
- // When in explanation mode, any key returns to select mode
- if (key.return || key.escape || input === "x") {
- setMode("select");
- }
- } else {
- // text entry mode
- if (key.return) {
- // if user hit enter on empty msg, fall back to DEFAULT_DENY_MESSAGE
- const custom = msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg;
- onReviewCommand(ReviewDecision.NO_CONTINUE, custom);
- } else if (key.escape) {
- // treat escape as denial with default message as well
- onReviewCommand(
- ReviewDecision.NO_CONTINUE,
- msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg,
- );
- }
- }
- });
+ },
+ { isActive },
+ );
return (
@@ -191,9 +210,13 @@ export function TerminalChatCommandReview({
Allow command?
);
@@ -417,12 +763,42 @@ export default function TerminalChatInput({
function TerminalChatInputThinking({
onInterrupt,
active,
+ thinkingSeconds,
}: {
onInterrupt: () => void;
active: boolean;
+ thinkingSeconds: number;
}) {
- const [dots, setDots] = useState("");
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
@@ -453,11 +829,9 @@ function TerminalChatInputThinking({
const str = Buffer.isBuffer(data) ? data.toString("utf8") : data;
if (str === "\x1b\x1b") {
// Treat as the first Escape press – prompt the user for confirmation.
- if (isLoggingEnabled()) {
- log(
- "raw stdin: received collapsed ESC ESC – starting confirmation timer",
- );
- }
+ log(
+ "raw stdin: received collapsed ESC ESC – starting confirmation timer",
+ );
setAwaitingConfirm(true);
setTimeout(() => setAwaitingConfirm(false), 1500);
}
@@ -470,10 +844,7 @@ function TerminalChatInputThinking({
};
}, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]);
- // Cycle the "Thinking…" animation dots.
- useInterval(() => {
- setDots((prev) => (prev.length < 3 ? prev + "." : ""));
- }, 500);
+ // 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
@@ -485,15 +856,11 @@ function TerminalChatInputThinking({
}
if (awaitingConfirm) {
- if (isLoggingEnabled()) {
- log("useInput: second ESC detected – triggering onInterrupt()");
- }
+ log("useInput: second ESC detected – triggering onInterrupt()");
onInterrupt();
setAwaitingConfirm(false);
} else {
- if (isLoggingEnabled()) {
- log("useInput: first ESC detected – waiting for confirmation");
- }
+ log("useInput: first ESC detected – waiting for confirmation");
setAwaitingConfirm(true);
setTimeout(() => setAwaitingConfirm(false), 1500);
}
@@ -504,8 +871,11 @@ function TerminalChatInputThinking({
return (
-
- Thinking{dots}
+ {frameWithSeconds}
+
+ Thinking
+ {dots}
+
{awaitingConfirm && (
diff --git a/codex-cli/src/components/chat/terminal-chat-new-input.tsx b/codex-cli/src/components/chat/terminal-chat-new-input.tsx
index 9edb4e63e0..b03cc9636d 100644
--- a/codex-cli/src/components/chat/terminal-chat-new-input.tsx
+++ b/codex-cli/src/components/chat/terminal-chat-new-input.tsx
@@ -8,16 +8,15 @@ import type {
import MultilineTextEditor from "./multiline-editor";
import { TerminalChatCommandReview } from "./terminal-chat-command-review.js";
-import { log, isLoggingEnabled } from "../../utils/agent/log.js";
import { loadConfig } from "../../utils/config.js";
import { createInputItem } from "../../utils/input-utils.js";
+import { log } from "../../utils/logger/log.js";
import { setSessionId } from "../../utils/session.js";
import {
loadCommandHistory,
addToHistory,
} from "../../utils/storage/command-history.js";
import { clearTerminal, onExit } from "../../utils/terminal.js";
-import Spinner from "../vendor/ink-spinner.js";
import { Box, Text, useApp, useInput, useStdin } from "ink";
import { fileURLToPath } from "node:url";
import React, { useCallback, useState, Fragment, useEffect } from "react";
@@ -37,39 +36,7 @@ const typeHelpText = `ctrl+c to exit | "/clear" to reset context | "/help" for c
const DEBUG_HIST =
process.env["DEBUG_TCI"] === "1" || process.env["DEBUG_TCI"] === "true";
-const thinkingTexts = ["Thinking"]; /* [
- "Consulting the rubber duck",
- "Maximizing paperclips",
- "Reticulating splines",
- "Immanentizing the Eschaton",
- "Thinking",
- "Thinking about thinking",
- "Spinning in circles",
- "Counting dust specks",
- "Updating priors",
- "Feeding the utility monster",
- "Taking off",
- "Wireheading",
- "Counting to infinity",
- "Staring into the Basilisk",
- "Running acausal tariff negotiations",
- "Searching the library of babel",
- "Multiplying matrices",
- "Solving the halting problem",
- "Counting grains of sand",
- "Simulating a simulation",
- "Asking the oracle",
- "Detangling qubits",
- "Reading tea leaves",
- "Pondering universal love and transcendent joy",
- "Feeling the AGI",
- "Shaving the yak",
- "Escaping local minima",
- "Pruning the search tree",
- "Descending the gradient",
- "Painting the bikeshed",
- "Securing funding",
-]; */
+// Placeholder for potential dynamic prompts – currently not used.
export default function TerminalChatInput({
isNew: _isNew,
@@ -85,8 +52,10 @@ export default function TerminalChatInput({
openModelOverlay,
openApprovalOverlay,
openHelpOverlay,
+ openDiffOverlay,
interruptAgent,
active,
+ thinkingSeconds,
}: {
isNew: boolean;
loading: boolean;
@@ -104,8 +73,10 @@ export default function TerminalChatInput({
openModelOverlay: () => void;
openApprovalOverlay: () => void;
openHelpOverlay: () => void;
+ openDiffOverlay: () => void;
interruptAgent: () => void;
active: boolean;
+ thinkingSeconds: number;
}): React.ReactElement {
const app = useApp();
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
@@ -261,6 +232,12 @@ export default function TerminalChatInput({
return;
}
+ if (inputValue === "/diff") {
+ setInput("");
+ openDiffOverlay();
+ return;
+ }
+
if (inputValue.startsWith("/model")) {
setInput("");
openModelOverlay();
@@ -286,17 +263,16 @@ export default function TerminalChatInput({
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) => [
- ...prev,
+ // Print a clear confirmation and reset conversation items.
+ setItems([
{
id: `clear-${Date.now()}`,
type: "message",
role: "system",
- content: [{ type: "input_text", text: "Context cleared" }],
+ content: [{ type: "input_text", text: "Terminal cleared" }],
},
]);
@@ -368,6 +344,7 @@ export default function TerminalChatInput({
openApprovalOverlay,
openModelOverlay,
openHelpOverlay,
+ openDiffOverlay,
history, // Add history to the dependency array
],
);
@@ -377,7 +354,11 @@ export default function TerminalChatInput({
);
}
@@ -389,6 +370,7 @@ export default function TerminalChatInput({
) : (
@@ -454,15 +436,43 @@ export default function TerminalChatInput({
function TerminalChatInputThinking({
onInterrupt,
active,
+ thinkingSeconds,
}: {
onInterrupt: () => void;
active: boolean;
+ thinkingSeconds: number;
}) {
- const [dots, setDots] = useState("");
const [awaitingConfirm, setAwaitingConfirm] = useState(false);
+ const [dots, setDots] = useState("");
- const [thinkingText] = useState(
- () => thinkingTexts[Math.floor(Math.random() * thinkingTexts.length)],
+ // Animate ellipsis
+ useInterval(() => {
+ setDots((prev) => (prev.length < 3 ? prev + "." : ""));
+ }, 500);
+
+ // Spinner frames with seconds embedded
+ const ballFrames = [
+ "( ● )",
+ "( ● )",
+ "( ● )",
+ "( ● )",
+ "( ●)",
+ "( ● )",
+ "( ● )",
+ "( ● )",
+ "( ● )",
+ "(● )",
+ ];
+ const [frame, setFrame] = useState(0);
+
+ useInterval(() => {
+ setFrame((idx) => (idx + 1) % ballFrames.length);
+ }, 80);
+
+ const frameTemplate = ballFrames[frame] ?? ballFrames[0];
+ const frameWithSeconds = (frameTemplate as string).replace(
+ "●",
+ `●${thinkingSeconds}s`,
);
// ---------------------------------------------------------------------
@@ -494,11 +504,9 @@ function TerminalChatInputThinking({
const str = Buffer.isBuffer(data) ? data.toString("utf8") : data;
if (str === "\x1b\x1b") {
// Treat as the first Escape press – prompt the user for confirmation.
- if (isLoggingEnabled()) {
- log(
- "raw stdin: received collapsed ESC ESC – starting confirmation timer",
- );
- }
+ log(
+ "raw stdin: received collapsed ESC ESC – starting confirmation timer",
+ );
setAwaitingConfirm(true);
setTimeout(() => setAwaitingConfirm(false), 1500);
}
@@ -511,9 +519,7 @@ function TerminalChatInputThinking({
};
}, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]);
- useInterval(() => {
- setDots((prev) => (prev.length < 3 ? prev + "." : ""));
- }, 500);
+ // Elapsed time provided via props – no local interval needed.
useInput(
(_input, key) => {
@@ -522,15 +528,11 @@ function TerminalChatInputThinking({
}
if (awaitingConfirm) {
- if (isLoggingEnabled()) {
- log("useInput: second ESC detected – triggering onInterrupt()");
- }
+ log("useInput: second ESC detected – triggering onInterrupt()");
onInterrupt();
setAwaitingConfirm(false);
} else {
- if (isLoggingEnabled()) {
- log("useInput: first ESC detected – waiting for confirmation");
- }
+ log("useInput: first ESC detected – waiting for confirmation");
setAwaitingConfirm(true);
setTimeout(() => setAwaitingConfirm(false), 1500);
}
@@ -541,9 +543,9 @@ function TerminalChatInputThinking({
return (
-
+ {frameWithSeconds}
- {thinkingText}
+ Thinking
{dots}
diff --git a/codex-cli/src/components/chat/terminal-chat-response-item.tsx b/codex-cli/src/components/chat/terminal-chat-response-item.tsx
index 4b7e069ff4..b1cc6edcb0 100644
--- a/codex-cli/src/components/chat/terminal-chat-response-item.tsx
+++ b/codex-cli/src/components/chat/terminal-chat-response-item.tsx
@@ -1,3 +1,4 @@
+import type { OverlayModeType } from "./terminal-chat";
import type { TerminalRendererOptions } from "marked-terminal";
import type {
ResponseFunctionToolCallItem,
@@ -14,18 +15,25 @@ import chalk, { type ForegroundColorName } from "chalk";
import { Box, Text } from "ink";
import { parse, setOptions } from "marked";
import TerminalRenderer from "marked-terminal";
-import React, { useMemo } from "react";
+import React, { useEffect, useMemo } from "react";
export default function TerminalChatResponseItem({
item,
fullStdout = false,
+ setOverlayMode,
}: {
item: ResponseItem;
fullStdout?: boolean;
+ setOverlayMode?: React.Dispatch>;
}): React.ReactElement {
switch (item.type) {
case "message":
- return ;
+ return (
+
+ );
case "function_call":
return ;
case "function_call_output":
@@ -98,9 +106,23 @@ const colorsByRole: Record = {
function TerminalChatResponseMessage({
message,
+ setOverlayMode,
}: {
message: ResponseInputMessageItem | ResponseOutputMessage;
+ setOverlayMode?: React.Dispatch>;
}) {
+ // 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 (
diff --git a/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx b/codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx
similarity index 100%
rename from codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx
rename to codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx
diff --git a/codex-cli/src/components/chat/terminal-chat-utils.ts b/codex-cli/src/components/chat/terminal-chat-utils.ts
deleted file mode 100644
index 73ab3c971a..0000000000
--- a/codex-cli/src/components/chat/terminal-chat-utils.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-import type { ResponseItem } from "openai/resources/responses/responses.mjs";
-
-import { approximateTokensUsed } from "../../utils/approximate-tokens-used.js";
-
-/**
- * Type‑guard that narrows a {@link ResponseItem} to one that represents a
- * user‑authored message. The OpenAI SDK represents both input *and* output
- * messages with a discriminated union where:
- * • `type` is the string literal "message" and
- * • `role` is one of "user" | "assistant" | "system" | "developer".
- *
- * For the purposes of de‑duplication we only care about *user* messages so we
- * detect those here in a single, reusable helper.
- */
-function isUserMessage(
- item: ResponseItem,
-): item is ResponseItem & { type: "message"; role: "user"; content: unknown } {
- return item.type === "message" && (item as { role?: string }).role === "user";
-}
-
-/**
- * Returns the maximum context length (in tokens) for a given model.
- * These numbers are best‑effort guesses and provide a basis for UI percentages.
- */
-export function maxTokensForModel(model: string): number {
- const lower = model.toLowerCase();
- if (lower.includes("32k")) {
- return 32000;
- }
- if (lower.includes("16k")) {
- return 16000;
- }
- if (lower.includes("8k")) {
- return 8000;
- }
- if (lower.includes("4k")) {
- return 4000;
- }
- // Default to 128k for newer long‑context models
- return 128000;
-}
-
-/**
- * Calculates the percentage of tokens remaining in context for a model.
- */
-export function calculateContextPercentRemaining(
- items: Array,
- model: string,
-): number {
- const used = approximateTokensUsed(items);
- const max = maxTokensForModel(model);
- const remaining = Math.max(0, max - used);
- return (remaining / max) * 100;
-}
-
-/**
- * Deduplicate the stream of {@link ResponseItem}s before they are persisted in
- * component state.
- *
- * Historically we used the (optional) {@code id} field returned by the
- * OpenAI streaming API as the primary key: the first occurrence of any given
- * {@code id} “won” and subsequent duplicates were dropped. In practice this
- * proved brittle because locally‑generated user messages don’t include an
- * {@code id}. The result was that if a user quickly pressed twice the
- * exact same message would appear twice in the transcript.
- *
- * The new rules are therefore:
- * 1. If a {@link ResponseItem} has an {@code id} keep only the *first*
- * occurrence of that {@code id} (this retains the previous behaviour for
- * assistant / tool messages).
- * 2. Additionally, collapse *consecutive* user messages with identical
- * content. Two messages are considered identical when their serialized
- * {@code content} array matches exactly. We purposefully restrict this
- * to **adjacent** duplicates so that legitimately repeated questions at
- * a later point in the conversation are still shown.
- */
-export function uniqueById(items: Array): Array {
- const seenIds = new Set();
- const deduped: Array = [];
-
- for (const item of items) {
- // ──────────────────────────────────────────────────────────────────
- // Rule #1 – de‑duplicate by id when present
- // ──────────────────────────────────────────────────────────────────
- if (typeof item.id === "string" && item.id.length > 0) {
- if (seenIds.has(item.id)) {
- continue; // skip duplicates
- }
- seenIds.add(item.id);
- }
-
- // ──────────────────────────────────────────────────────────────────
- // Rule #2 – collapse consecutive identical user messages
- // ──────────────────────────────────────────────────────────────────
- if (isUserMessage(item) && deduped.length > 0) {
- const prev = deduped[deduped.length - 1]!;
-
- if (
- isUserMessage(prev) &&
- // Note: the `content` field is an array of message parts. Performing
- // a deep compare is over‑kill here; serialising to JSON is sufficient
- // (and fast for the tiny payloads involved).
- JSON.stringify(prev.content) === JSON.stringify(item.content)
- ) {
- continue; // skip duplicate user message
- }
- }
-
- deduped.push(item);
- }
-
- return deduped;
-}
diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx
index 50ee447932..6a1cfe1b84 100644
--- a/codex-cli/src/components/chat/terminal-chat.tsx
+++ b/codex-cli/src/components/chat/terminal-chat.tsx
@@ -5,35 +5,46 @@ import type { ColorName } from "chalk";
import type { ResponseItem } from "openai/resources/responses/responses.mjs";
import TerminalChatInput from "./terminal-chat-input.js";
-import { TerminalChatToolCallCommand } from "./terminal-chat-tool-call-item.js";
-import {
- calculateContextPercentRemaining,
- uniqueById,
-} from "./terminal-chat-utils.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 { isLoggingEnabled, log } from "../../utils/agent/log.js";
import { ReviewDecision } from "../../utils/agent/review.js";
import { generateCompactSummary } from "../../utils/compact-summary.js";
-import { OPENAI_BASE_URL } from "../../utils/config.js";
+import { getBaseUrl, getApiKey, 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 { getAvailableModels } from "../../utils/model-utils.js";
+import { log } from "../../utils/logger/log.js";
+import {
+ getAvailableModels,
+ calculateContextPercentRemaining,
+ uniqueById,
+} from "../../utils/model-utils.js";
import { CLI_VERSION } from "../../utils/session.js";
import { shortCwd } from "../../utils/short-path.js";
import { saveRollout } from "../../utils/storage/save-rollout.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 { Box, Text } from "ink";
-import { exec } from "node:child_process";
+import { spawn } from "node:child_process";
import OpenAI from "openai";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { inspect } from "util";
+export type OverlayModeType =
+ | "none"
+ | "history"
+ | "model"
+ | "approval"
+ | "help"
+ | "diff";
+
type Props = {
config: AppConfig;
prompt?: string;
@@ -54,17 +65,21 @@ const colorsByPolicy: Record = {
*
* @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 = new OpenAI({
- apiKey: process.env["OPENAI_API_KEY"],
- baseURL: OPENAI_BASE_URL,
+ apiKey: getApiKey(config.provider),
+ baseURL: getBaseUrl(config.provider),
});
// Format the command for display
@@ -73,6 +88,7 @@ async function generateCommandExplanation(
// 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",
@@ -93,11 +109,8 @@ async function generateCommandExplanation(
} catch (error) {
log(`Error generating command explanation: ${error}`);
- // Improved error handling with more specific error information
let errorMessage = "Unable to generate explanation due to an error.";
-
if (error instanceof Error) {
- // Include specific error message for better debugging
errorMessage = `Unable to generate explanation: ${error.message}`;
// If it's an API error, check for more specific information
@@ -128,21 +141,26 @@ export default function TerminalChat({
additionalWritableRoots,
fullStdout,
}: Props): React.ReactElement {
- // Desktop notification setting
const notify = 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);
- // Allow switching approval modes at runtime via an overlay.
const [approvalPolicy, setApprovalPolicy] = useState(
initialApprovalPolicy,
);
const [thinkingSeconds, setThinkingSeconds] = useState(0);
+
const handleCompact = async () => {
setLoading(true);
try {
- const summary = await generateCompactSummary(items, model);
+ const summary = await generateCompactSummary(
+ items,
+ model,
+ Boolean(config.flexMode),
+ config,
+ );
setItems([
{
id: `compact-${Date.now()}`,
@@ -167,15 +185,21 @@ export default function TerminalChat({
setLoading(false);
}
};
+
const {
requestConfirmation,
confirmationPrompt,
explanation,
submitConfirmation,
} = useConfirmation();
- const [overlayMode, setOverlayMode] = useState<
- "none" | "history" | "model" | "approval" | "help"
- >("none");
+ const [overlayMode, setOverlayMode] = useState("none");
+
+ // 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] =
@@ -191,39 +215,44 @@ export default function TerminalChat({
// ────────────────────────────────────────────────────────────────
// DEBUG: log every render w/ key bits of state
// ────────────────────────────────────────────────────────────────
- if (isLoggingEnabled()) {
- log(
- `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${
- items.length
- }`,
- );
- }
+ log(
+ `render - agent? ${Boolean(agentRef.current)} loading=${loading} items=${
+ items.length
+ }`,
+ );
useEffect(() => {
- if (isLoggingEnabled()) {
- log("creating NEW AgentLoop");
- log(
- `model=${model} instructions=${Boolean(
- config.instructions,
- )} approvalPolicy=${approvalPolicy}`,
- );
+ // Skip recreating the agent if awaiting a decision on a pending confirmation.
+ if (confirmationPrompt != null) {
+ log("skip AgentLoop recreation due to pending confirmationPrompt");
+ return;
}
- // Tear down any existing loop before creating a new one
+ 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(updated);
+ saveRollout(sessionId, updated);
return updated;
});
},
@@ -240,15 +269,18 @@ export default function TerminalChat({
,
);
- // If the user wants an explanation, generate one and ask again
+ // If the user wants an explanation, generate one and ask again.
if (review === ReviewDecision.EXPLAIN) {
log(`Generating explanation for command: ${commandForDisplay}`);
-
- // Generate an explanation using the same model
- const explanation = await generateCommandExplanation(command, model);
+ const explanation = await generateCommandExplanation(
+ command,
+ model,
+ Boolean(config.flexMode),
+ config,
+ );
log(`Generated explanation: ${explanation}`);
- // Ask for confirmation again, but with the explanation
+ // Ask for confirmation again, but with the explanation.
const confirmResult = await requestConfirmation(
,
);
- // Update the decision based on the second confirmation
+ // Update the decision based on the second confirmation.
review = confirmResult.decision;
customDenyMessage = confirmResult.customDenyMessage;
- // Return the final decision with the explanation
+ // Return the final decision with the explanation.
return { review, customDenyMessage, applyPatch, explanation };
}
@@ -268,30 +300,23 @@ export default function TerminalChat({
},
});
- // force a render so JSX below can "see" the freshly created agent
+ // Force a render so JSX below can "see" the freshly created agent.
forceUpdate();
- if (isLoggingEnabled()) {
- log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`);
- }
+ log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`);
return () => {
- if (isLoggingEnabled()) {
- log("terminating AgentLoop");
- }
+ log("terminating AgentLoop");
agentRef.current?.terminate();
agentRef.current = undefined;
forceUpdate(); // re‑render after teardown too
};
- }, [
- model,
- config,
- approvalPolicy,
- requestConfirmation,
- additionalWritableRoots,
- ]);
+ // 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
+ // 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(() => {
@@ -316,14 +341,15 @@ export default function TerminalChat({
};
}, [loading, confirmationPrompt]);
- // Notify desktop with a preview when an assistant response arrives
+ // Notify desktop with a preview when an assistant response arrives.
const prevLoadingRef = useRef(false);
useEffect(() => {
- // Only notify when notifications are enabled
+ // Only notify when notifications are enabled.
if (!notify) {
prevLoadingRef.current = loading;
return;
}
+
if (
prevLoadingRef.current &&
!loading &&
@@ -350,21 +376,20 @@ export default function TerminalChat({
const safePreview = preview.replace(/"/g, '\\"');
const title = "Codex CLI";
const cwd = PWD;
- exec(
- `osascript -e 'display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"'`,
- );
+ 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
+ // Let's also track whenever the ref becomes available.
const agent = agentRef.current;
useEffect(() => {
- if (isLoggingEnabled()) {
- log(`agentRef.current is now ${Boolean(agent)}`);
- }
+ log(`agentRef.current is now ${Boolean(agent)}`);
}, [agent]);
// ---------------------------------------------------------------------
@@ -384,7 +409,7 @@ export default function TerminalChat({
const inputItems = [
await createInputItem(initialPrompt || "", initialImagePaths || []),
];
- // Clear them to prevent subsequent runs
+ // Clear them to prevent subsequent runs.
setInitialPrompt("");
setInitialImagePaths([]);
agent?.run(inputItems);
@@ -397,7 +422,7 @@ export default function TerminalChat({
// ────────────────────────────────────────────────────────────────
useEffect(() => {
(async () => {
- const available = await getAvailableModels();
+ const available = await getAvailableModels(provider);
if (model && available.length > 0 && !available.includes(model)) {
setItems((prev) => [
...prev,
@@ -408,7 +433,7 @@ export default function TerminalChat({
content: [
{
type: "input_text",
- text: `Warning: model "${model}" is not in the list of available models returned by OpenAI.`,
+ text: `Warning: model "${model}" is not in the list of available models for provider "${provider}".`,
},
],
},
@@ -419,7 +444,7 @@ export default function TerminalChat({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- // Just render every item in order, no grouping/collapse
+ // Just render every item in order, no grouping/collapse.
const lastMessageBatch = items.map((item) => ({ item }));
const groupCounts: Record = {};
const userMsgCount = items.filter(
@@ -436,6 +461,7 @@ export default function TerminalChat({
{agent ? (
) : (
@@ -460,7 +488,7 @@ export default function TerminalChat({
Initializing agent…
)}
- {agent && (
+ {overlayMode === "none" && agent && (
setOverlayMode("model")}
openApprovalOverlay={() => setOverlayMode("approval")}
openHelpOverlay={() => setOverlayMode("help")}
+ 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;
}
- if (isLoggingEnabled()) {
- log(
- "TerminalChat: interruptAgent invoked – calling agent.cancel()",
- );
- }
+ log(
+ "TerminalChat: interruptAgent invoked – calling agent.cancel()",
+ );
agent.cancel();
setLoading(false);
@@ -516,6 +562,8 @@ export default function TerminalChat({
agent.run(inputs, lastResponseId || "");
return {};
}}
+ items={items}
+ thinkingSeconds={thinkingSeconds}
/>
)}
{overlayMode === "history" && (
@@ -524,15 +572,15 @@ export default function TerminalChat({
{overlayMode === "model" && (
{
- if (isLoggingEnabled()) {
- log(
- "TerminalChat: interruptAgent invoked – calling agent.cancel()",
- );
- if (!agent) {
- log("TerminalChat: agent is not ready yet");
- }
+ log(
+ "TerminalChat: interruptAgent invoked – calling agent.cancel()",
+ );
+ if (!agent) {
+ log("TerminalChat: agent is not ready yet");
}
agent?.cancel();
setLoading(false);
@@ -542,6 +590,13 @@ export default function TerminalChat({
prev && newModel !== model ? null : prev,
);
+ // Save model to config
+ saveConfig({
+ ...config,
+ model: newModel,
+ provider: provider,
+ });
+
setItems((prev) => [
...prev,
{
@@ -559,6 +614,51 @@ export default function TerminalChat({
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")}
/>
)}
@@ -567,12 +667,19 @@ export default function TerminalChat({
{
- agent?.cancel();
- setLoading(false);
+ // 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,
{
@@ -597,6 +704,13 @@ export default function TerminalChat({
{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
index 3c8e3089ca..1bd08aef2a 100644
--- a/codex-cli/src/components/chat/terminal-header.tsx
+++ b/codex-cli/src/components/chat/terminal-header.tsx
@@ -9,10 +9,12 @@ export interface TerminalHeaderProps {
version: string;
PWD: string;
model: string;
+ provider?: string;
approvalPolicy: string;
colorsByPolicy: Record;
agent?: AgentLoop;
initialImagePaths?: Array;
+ flexModeEnabled?: boolean;
}
const TerminalHeader: React.FC = ({
@@ -20,18 +22,21 @@ const TerminalHeader: React.FC = ({
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} –{" "}
+ ● Codex v{version} - {PWD} - {model} ({provider}) -{" "}
{approvalPolicy}
+ {flexModeEnabled ? " - flex-mode" : ""}
) : (
<>
@@ -62,12 +67,22 @@ const TerminalHeader: React.FC = ({
↳ model: {model}
+
+ ↳ provider:{" "}
+ {provider}
+
↳ approval:{" "}
{approvalPolicy}
+ {flexModeEnabled && (
+
+ ↳ flex-mode:{" "}
+ enabled
+
+ )}
{initialImagePaths?.map((img, idx) => (
↳ image:{" "}
diff --git a/codex-cli/src/components/chat/terminal-message-history.tsx b/codex-cli/src/components/chat/terminal-message-history.tsx
index 07c3ac3762..8171f629a8 100644
--- a/codex-cli/src/components/chat/terminal-message-history.tsx
+++ b/codex-cli/src/components/chat/terminal-message-history.tsx
@@ -1,17 +1,18 @@
+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 TerminalChatResponseItem from "./terminal-chat-response-item.js";
import TerminalHeader from "./terminal-header.js";
-import { Box, Static, Text } from "ink";
+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 MessageHistoryProps = {
+type TerminalMessageHistoryProps = {
batch: Array;
groupCounts: Record;
items: Array;
@@ -21,25 +22,25 @@ type MessageHistoryProps = {
thinkingSeconds: number;
headerProps: TerminalHeaderProps;
fullStdout: boolean;
+ setOverlayMode: React.Dispatch>;
};
-const MessageHistory: React.FC = ({
+const TerminalMessageHistory: React.FC = ({
batch,
headerProps,
- loading,
- thinkingSeconds,
+ // `loading` and `thinkingSeconds` handled by input component now.
+ loading: _loading,
+ thinkingSeconds: _thinkingSeconds,
fullStdout,
+ setOverlayMode,
}) => {
// Flatten batch entries to response items.
const messages = useMemo(() => batch.map(({ item }) => item!), [batch]);
return (
- {loading && (
-
- {`thinking for ${thinkingSeconds}s`}
-
- )}
+ {/* 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") {
@@ -67,6 +68,7 @@ const MessageHistory: React.FC = ({
);
@@ -76,4 +78,4 @@ const MessageHistory: React.FC = ({
);
};
-export default React.memo(MessageHistory);
+export default React.memo(TerminalMessageHistory);
diff --git a/codex-cli/src/components/diff-overlay.tsx b/codex-cli/src/components/diff-overlay.tsx
new file mode 100644
index 0000000000..8de85b87d5
--- /dev/null
+++ b/codex-cli/src/components/diff-overlay.tsx
@@ -0,0 +1,93 @@
+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
index 9feabc16aa..6eeffb9efb 100644
--- a/codex-cli/src/components/help-overlay.tsx
+++ b/codex-cli/src/components/help-overlay.tsx
@@ -52,6 +52,12 @@ export default function HelpOverlay({
/clearhistory – clear command history
+
+ /bug – file a bug report with session log
+
+
+ /diff – view working tree git diff
+
/compact – condense context into a summary
diff --git a/codex-cli/src/components/history-overlay.tsx b/codex-cli/src/components/history-overlay.tsx
index c22e22d87d..9cd2d6d058 100644
--- a/codex-cli/src/components/history-overlay.tsx
+++ b/codex-cli/src/components/history-overlay.tsx
@@ -14,7 +14,10 @@ export default function HistoryOverlay({ items, onExit }: Props): JSX.Element {
const [mode, setMode] = useState("commands");
const [cursor, setCursor] = useState(0);
- const { commands, files } = useMemo(() => buildLists(items), [items]);
+ const { commands, files } = useMemo(
+ () => formatHistoryForDisplay(items),
+ [items],
+ );
const list = mode === "commands" ? commands : files;
@@ -95,7 +98,7 @@ export default function HistoryOverlay({ items, onExit }: Props): JSX.Element {
);
}
-function buildLists(items: Array): {
+function formatHistoryForDisplay(items: Array): {
commands: Array;
files: Array;
} {
@@ -103,33 +106,9 @@ function buildLists(items: Array): {
const filesSet = new Set();
for (const item of items) {
- 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.
- const truncated =
- fullPrompt.length > 120 ? `${fullPrompt.slice(0, 117)}…` : fullPrompt;
- commands.push(`> ${truncated}`);
- }
-
+ const userPrompt = processUserMessage(item);
+ if (userPrompt) {
+ commands.push(userPrompt);
continue;
}
@@ -173,31 +152,7 @@ function buildLists(items: Array): {
: undefined;
if (cmdArray && cmdArray.length > 0) {
- commands.push(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]);
- }
- }
- }
- }
-
+ commands.push(processCommandArray(cmdArray, filesSet));
continue; // We processed this as a command; no need to treat as generic tool call.
}
@@ -205,33 +160,96 @@ function buildLists(items: Array): {
// short argument representation to give users an idea of what
// happened.
if (typeof toolName === "string" && toolName.length > 0) {
- 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;
- }
- }
- }
-
- commands.push(summary);
+ 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
index c006eb3193..c9dde0e6b4 100644
--- a/codex-cli/src/components/model-overlay.tsx
+++ b/codex-cli/src/components/model-overlay.tsx
@@ -1,7 +1,7 @@
import TypeaheadOverlay from "./typeahead-overlay.js";
import {
getAvailableModels,
- RECOMMENDED_MODELS,
+ RECOMMENDED_MODELS as _RECOMMENDED_MODELS,
} from "../utils/model-utils.js";
import { Box, Text, useInput } from "ink";
import React, { useEffect, useState } from "react";
@@ -16,39 +16,53 @@ import React, { useEffect, useState } from "react";
*/
type Props = {
currentModel: string;
+ currentProvider?: string;
hasLastResponse: boolean;
+ providers?: Record;
onSelect: (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 () => {
- const models = await getAvailableModels();
-
- // Split the list into recommended and “other” models.
- const recommended = RECOMMENDED_MODELS.filter((m) => models.includes(m));
- const others = models.filter((m) => !recommended.includes(m));
-
- const ordered = [...recommended, ...others.sort()];
-
- setItems(
- ordered.map((m) => ({
- label: recommended.includes(m) ? `⭐ ${m}` : m,
- value: m,
- })),
- );
+ 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
@@ -58,10 +72,14 @@ export default function ModelOverlay({
// available action is to dismiss the overlay (Esc or Enter).
// ---------------------------------------------------------------------------
- // Always register input handling so hooks are called consistently.
+ // 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");
+ }
}
});
@@ -91,13 +109,47 @@ export default function ModelOverlay({
);
}
+ 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 model: {currentModel}
+
+
+ Current provider: {currentProvider}
+
+ {isLoading && Loading models... }
+ press tab to switch to provider selection
+
}
initialItems={items}
currentValue={currentModel}
diff --git a/codex-cli/src/components/select-input/Indicator.tsx b/codex-cli/src/components/select-input/indicator.tsx
similarity index 100%
rename from codex-cli/src/components/select-input/Indicator.tsx
rename to codex-cli/src/components/select-input/indicator.tsx
diff --git a/codex-cli/src/components/select-input/Item.tsx b/codex-cli/src/components/select-input/item.tsx
similarity index 100%
rename from codex-cli/src/components/select-input/Item.tsx
rename to codex-cli/src/components/select-input/item.tsx
diff --git a/codex-cli/src/components/select-input/select-input.tsx b/codex-cli/src/components/select-input/select-input.tsx
index 264f3f9e1c..701c655d37 100644
--- a/codex-cli/src/components/select-input/select-input.tsx
+++ b/codex-cli/src/components/select-input/select-input.tsx
@@ -1,5 +1,5 @@
-import Indicator, { type Props as IndicatorProps } from "./Indicator.js";
-import ItemComponent, { type Props as ItemProps } from "./Item.js";
+import Indicator, { type Props as IndicatorProps } from "./indicator.js";
+import ItemComponent, { type Props as ItemProps } from "./item.js";
import isEqual from "fast-deep-equal";
import { Box, useInput } from "ink";
import React, {
diff --git a/codex-cli/src/components/singlepass-cli-app.tsx b/codex-cli/src/components/singlepass-cli-app.tsx
index 63982bf468..56d1d913b4 100644
--- a/codex-cli/src/components/singlepass-cli-app.tsx
+++ b/codex-cli/src/components/singlepass-cli-app.tsx
@@ -5,7 +5,7 @@ import type { FileOperation } from "../utils/singlepass/file_ops";
import Spinner from "./vendor/ink-spinner"; // Third‑party / vendor components
import TextInput from "./vendor/ink-text-input";
-import { OPENAI_TIMEOUT_MS, OPENAI_BASE_URL } from "../utils/config";
+import { OPENAI_TIMEOUT_MS, getBaseUrl, getApiKey } from "../utils/config";
import {
generateDiffSummary,
generateEditSummary,
@@ -394,12 +394,13 @@ export function SinglePassApp({
});
const openai = new OpenAI({
- apiKey: config.apiKey ?? "",
- baseURL: OPENAI_BASE_URL || undefined,
+ apiKey: getApiKey(config.provider),
+ baseURL: getBaseUrl(config.provider),
timeout: OPENAI_TIMEOUT_MS,
});
const chatResp = await openai.beta.chat.completions.parse({
model: config.model,
+ ...(config.flexMode ? { service_tier: "flex" } : {}),
messages: [
{
role: "user",
diff --git a/codex-cli/src/components/vendor/ink-text-input.tsx b/codex-cli/src/components/vendor/ink-text-input.tsx
index c516799a70..9c015be953 100644
--- a/codex-cli/src/components/vendor/ink-text-input.tsx
+++ b/codex-cli/src/components/vendor/ink-text-input.tsx
@@ -44,6 +44,11 @@ export type TextInputProps = {
* Function to call when `Enter` is pressed, where first argument is a value of the input.
*/
readonly onSubmit?: (value: string) => void;
+
+ /**
+ * Explicitly set the cursor position to the end of the text
+ */
+ readonly cursorToEnd?: boolean;
};
function findPrevWordJump(prompt: string, cursorOffset: number) {
@@ -90,12 +95,22 @@ function TextInput({
showCursor = true,
onChange,
onSubmit,
+ cursorToEnd = false,
}: TextInputProps) {
const [state, setState] = useState({
cursorOffset: (originalValue || "").length,
cursorWidth: 0,
});
+ useEffect(() => {
+ if (cursorToEnd) {
+ setState((prev) => ({
+ ...prev,
+ cursorOffset: (originalValue || "").length,
+ }));
+ }
+ }, [cursorToEnd, originalValue, focus]);
+
const { cursorOffset, cursorWidth } = state;
useEffect(() => {
@@ -153,6 +168,78 @@ function TextInput({
useInput(
(input, key) => {
+ // ────────────────────────────────────────────────────────────────
+ // Support Shift+Enter / Ctrl+Enter from terminals that have
+ // modifyOtherKeys enabled. Such terminals encode the key‑combo in a
+ // CSI sequence rather than sending a bare "\r"/"\n". Ink passes the
+ // sequence through as raw text (without the initial ESC), so we need to
+ // detect and translate it before the generic character handler below
+ // treats it as literal input (e.g. "[27;2;13~"). We support both the
+ // modern *mode 2* (CSI‑u, ending in "u") and the legacy *mode 1*
+ // variant (ending in "~").
+ //
+ // - Shift+Enter → insert newline (same behaviour as Option+Enter)
+ // - Ctrl+Enter → submit the input (same as plain Enter)
+ //
+ // References: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Modify-Other-Keys
+ // ────────────────────────────────────────────────────────────────
+
+ function handleEncodedEnterSequence(raw: string): boolean {
+ // CSI‑u (modifyOtherKeys=2) → "[13;u"
+ let m = raw.match(/^\[([0-9]+);([0-9]+)u$/);
+ if (m && m[1] === "13") {
+ const mod = Number(m[2]);
+ const hasCtrl = Math.floor(mod / 4) % 2 === 1;
+
+ if (hasCtrl) {
+ if (onSubmit) {
+ onSubmit(originalValue);
+ }
+ } else {
+ const newValue =
+ originalValue.slice(0, cursorOffset) +
+ "\n" +
+ originalValue.slice(cursorOffset);
+
+ setState({
+ cursorOffset: cursorOffset + 1,
+ cursorWidth: 0,
+ });
+ onChange(newValue);
+ }
+ return true; // handled
+ }
+
+ // CSI‑~ (modifyOtherKeys=1) → "[27;;13~"
+ m = raw.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(originalValue);
+ }
+ } else {
+ const newValue =
+ originalValue.slice(0, cursorOffset) +
+ "\n" +
+ originalValue.slice(cursorOffset);
+
+ setState({
+ cursorOffset: cursorOffset + 1,
+ cursorWidth: 0,
+ });
+ onChange(newValue);
+ }
+ return true; // handled
+ }
+ return false; // not an encoded Enter sequence
+ }
+
+ if (handleEncodedEnterSequence(input)) {
+ return;
+ }
if (
key.upArrow ||
key.downArrow ||
diff --git a/codex-cli/src/hooks/use-confirmation.ts b/codex-cli/src/hooks/use-confirmation.ts
index b10a309dc5..07c9e75cd0 100644
--- a/codex-cli/src/hooks/use-confirmation.ts
+++ b/codex-cli/src/hooks/use-confirmation.ts
@@ -1,4 +1,3 @@
-// use-confirmation.ts
import type { ReviewDecision } from "../utils/agent/review";
import type React from "react";
diff --git a/codex-cli/src/shims-external.d.ts b/codex-cli/src/shims-external.d.ts
new file mode 100644
index 0000000000..95530e6110
--- /dev/null
+++ b/codex-cli/src/shims-external.d.ts
@@ -0,0 +1,24 @@
+// Ambient module declarations for optional/runtime‑only dependencies so that
+// `tsc --noEmit` succeeds without installing their full type definitions.
+
+declare module "package-manager-detector" {
+ export type AgentName = "npm" | "pnpm" | "yarn" | "bun" | "deno";
+
+ /** Detects the package manager based on environment variables. */
+ export function getUserAgent(): AgentName | null | undefined;
+}
+
+declare module "fast-npm-meta" {
+ export interface LatestVersionMeta {
+ version: string;
+ }
+
+ export function getLatestVersion(
+ pkgName: string,
+ opts?: Record,
+ ): Promise;
+}
+
+declare module "semver" {
+ export function gt(v1: string, v2: string): boolean;
+}
diff --git a/codex-cli/src/text-buffer.ts b/codex-cli/src/text-buffer.ts
index d7ce153060..fe4e2a4759 100644
--- a/codex-cli/src/text-buffer.ts
+++ b/codex-cli/src/text-buffer.ts
@@ -423,7 +423,7 @@ export default class TextBuffer {
/** Delete the word to the *left* of the caret, mirroring common
* Ctrl/Alt+Backspace behaviour in editors & terminals. Both the adjacent
* whitespace *and* the word characters immediately preceding the caret are
- * removed. If the caret is already at column‑0 this becomes a no‑op. */
+ * removed. If the caret is already at column‑0 this becomes a no-op. */
deleteWordLeft(): void {
dbg("deleteWordLeft", { beforeCursor: this.getCursor() });
@@ -710,7 +710,7 @@ export default class TextBuffer {
}
endSelection(): void {
- // no‑op for now, kept for API symmetry
+ // no-op for now, kept for API symmetry
// we rely on anchor + current cursor to compute selection
}
diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts
index 4508553a1b..c16bfd6719 100644
--- a/codex-cli/src/utils/agent/agent-loop.ts
+++ b/codex-cli/src/utils/agent/agent-loop.ts
@@ -1,16 +1,20 @@
import type { ReviewDecision } from "./review.js";
import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js";
import type { AppConfig } from "../config.js";
+import type { ResponseEvent } from "../responses.js";
import type {
ResponseFunctionToolCall,
ResponseInputItem,
ResponseItem,
+ ResponseCreateParams,
+ FunctionTool,
} from "openai/resources/responses/responses.mjs";
import type { Reasoning } from "openai/resources.mjs";
-import { log, isLoggingEnabled } from "./log.js";
-import { OPENAI_BASE_URL, OPENAI_TIMEOUT_MS } from "../config.js";
+import { OPENAI_TIMEOUT_MS, getApiKey, getBaseUrl } from "../config.js";
+import { log } from "../logger/log.js";
import { parseToolCallArguments } from "../parsers.js";
+import { responsesCreateViaChatCompletions } from "../responses.js";
import {
ORIGIN,
CLI_VERSION,
@@ -39,9 +43,18 @@ const alreadyProcessedResponses = new Set();
type AgentLoopParams = {
model: string;
+ provider?: string;
config?: AppConfig;
instructions?: string;
approvalPolicy: ApprovalPolicy;
+ /**
+ * Whether the model responses should be stored on the server side (allows
+ * using `previous_response_id` to provide conversational context). Defaults
+ * to `true` to preserve the current behaviour. When set to `false` the agent
+ * will instead send the *full* conversation context as the `input` payload
+ * on every request and omit the `previous_response_id` parameter.
+ */
+ disableResponseStorage?: boolean;
onItem: (item: ResponseItem) => void;
onLoading: (loading: boolean) => void;
@@ -56,12 +69,39 @@ type AgentLoopParams = {
onLastResponseId: (lastResponseId: string) => void;
};
+const shellTool: FunctionTool = {
+ type: "function",
+ name: "shell",
+ description: "Runs a shell command, and returns its output.",
+ strict: false,
+ parameters: {
+ type: "object",
+ properties: {
+ command: { type: "array", items: { type: "string" } },
+ workdir: {
+ type: "string",
+ description: "The working directory for the command.",
+ },
+ timeout: {
+ type: "number",
+ description:
+ "The maximum time to wait for the command to complete in milliseconds.",
+ },
+ },
+ required: ["command"],
+ additionalProperties: false,
+ },
+};
+
export class AgentLoop {
private model: string;
+ private provider: string;
private instructions?: string;
private approvalPolicy: ApprovalPolicy;
private config: AppConfig;
private additionalWritableRoots: ReadonlyArray;
+ /** Whether we ask the API to persist conversation state on the server */
+ private readonly disableResponseStorage: boolean;
// Using `InstanceType` sidesteps typing issues with the OpenAI package under
// the TS 5+ `moduleResolution=bundler` setup. OpenAI client instance. We keep the concrete
@@ -92,6 +132,13 @@ export class AgentLoop {
private execAbortController: AbortController | null = null;
/** Set to true when `cancel()` is called so `run()` can exit early. */
private canceled = false;
+
+ /**
+ * Local conversation transcript used when `disableResponseStorage === true`. Holds
+ * all non‑system items exchanged so far so we can provide full context on
+ * every request.
+ */
+ private transcript: Array = [];
/** Function calls that were emitted by the model but never answered because
* the user cancelled the run. We keep the `call_id`s around so the *next*
* request can send a dummy `function_call_output` that satisfies the
@@ -116,15 +163,13 @@ export class AgentLoop {
// Reset the current stream to allow new requests
this.currentStream = null;
- if (isLoggingEnabled()) {
- log(
- `AgentLoop.cancel() invoked – currentStream=${Boolean(
- this.currentStream,
- )} execAbortController=${Boolean(
- this.execAbortController,
- )} generation=${this.generation}`,
- );
- }
+ log(
+ `AgentLoop.cancel() invoked – currentStream=${Boolean(
+ this.currentStream,
+ )} execAbortController=${Boolean(this.execAbortController)} generation=${
+ this.generation
+ }`,
+ );
(
this.currentStream as { controller?: { abort?: () => void } } | null
)?.controller?.abort?.();
@@ -136,9 +181,7 @@ export class AgentLoop {
// Create a new abort controller for future tool calls
this.execAbortController = new AbortController();
- if (isLoggingEnabled()) {
- log("AgentLoop.cancel(): execAbortController.abort() called");
- }
+ log("AgentLoop.cancel(): execAbortController.abort() called");
// NOTE: We intentionally do *not* clear `lastResponseId` here. If the
// stream produced a `function_call` before the user cancelled, OpenAI now
@@ -174,9 +217,7 @@ export class AgentLoop {
// this.onItem(cancelNotice);
this.generation += 1;
- if (isLoggingEnabled()) {
- log(`AgentLoop.cancel(): generation bumped to ${this.generation}`);
- }
+ log(`AgentLoop.cancel(): generation bumped to ${this.generation}`);
}
/**
@@ -204,8 +245,10 @@ export class AgentLoop {
// private cumulativeThinkingMs = 0;
constructor({
model,
+ provider = "openai",
instructions,
approvalPolicy,
+ disableResponseStorage,
// `config` used to be required. Some unit‑tests (and potentially other
// callers) instantiate `AgentLoop` without passing it, so we make it
// optional and fall back to sensible defaults. This keeps the public
@@ -220,6 +263,7 @@ export class AgentLoop {
additionalWritableRoots,
}: AgentLoopParams & { config?: AppConfig }) {
this.model = model;
+ this.provider = provider;
this.instructions = instructions;
this.approvalPolicy = approvalPolicy;
@@ -239,10 +283,14 @@ export class AgentLoop {
this.onLoading = onLoading;
this.getCommandConfirmation = getCommandConfirmation;
this.onLastResponseId = onLastResponseId;
+
+ this.disableResponseStorage = disableResponseStorage ?? false;
this.sessionId = getSessionId() || randomUUID().replaceAll("-", "");
// Configure OpenAI client with optional timeout (ms) from environment
const timeoutMs = OPENAI_TIMEOUT_MS;
- const apiKey = this.config.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
+ const apiKey = getApiKey(this.provider);
+ const baseURL = getBaseUrl(this.provider);
+
this.oai = new OpenAI({
// The OpenAI JS SDK only requires `apiKey` when making requests against
// the official API. When running unit‑tests we stub out all network
@@ -251,7 +299,7 @@ export class AgentLoop {
// errors inside the SDK (it validates that `apiKey` is a non‑empty
// string when the field is present).
...(apiKey ? { apiKey } : {}),
- baseURL: OPENAI_BASE_URL,
+ baseURL,
defaultHeaders: {
originator: ORIGIN,
version: CLI_VERSION,
@@ -315,13 +363,11 @@ export class AgentLoop {
const callId: string = (item as any).call_id ?? (item as any).id;
const args = parseToolCallArguments(rawArguments ?? "{}");
- if (isLoggingEnabled()) {
- log(
- `handleFunctionCall(): name=${
- name ?? "undefined"
- } callId=${callId} args=${rawArguments}`,
- );
- }
+ log(
+ `handleFunctionCall(): name=${
+ name ?? "undefined"
+ } callId=${callId} args=${rawArguments}`,
+ );
if (args == null) {
const outputItem: ResponseInputItem.FunctionCallOutput = {
@@ -407,11 +453,9 @@ export class AgentLoop {
// Create a fresh AbortController for this run so that tool calls from a
// previous run do not accidentally get signalled.
this.execAbortController = new AbortController();
- if (isLoggingEnabled()) {
- log(
- `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`,
- );
- }
+ log(
+ `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`,
+ );
// NOTE: We no longer (re‑)attach an `abort` listener to `hardAbort` here.
// A single listener that forwards the `abort` to the current
// `execAbortController` is installed once in the constructor. Re‑adding a
@@ -419,7 +463,17 @@ export class AgentLoop {
// accumulate listeners which in turn triggered Node's
// `MaxListenersExceededWarning` after ten invocations.
- let lastResponseId: string = previousResponseId;
+ // Track the response ID from the last *stored* response so we can use
+ // `previous_response_id` when `disableResponseStorage` is enabled. When storage
+ // is disabled we deliberately ignore the caller‑supplied value because
+ // the backend will not retain any state that could be referenced.
+ // If the backend stores conversation state (`disableResponseStorage === false`) we
+ // forward the caller‑supplied `previousResponseId` so that the model sees the
+ // full context. When storage is disabled we *must not* send any ID because the
+ // server no longer retains the referenced response.
+ let lastResponseId: string = this.disableResponseStorage
+ ? ""
+ : previousResponseId;
// If there are unresolved function calls from a previously cancelled run
// we have to emit dummy tool outputs so that the API no longer expects
@@ -441,7 +495,55 @@ export class AgentLoop {
this.pendingAborts.clear();
}
- let turnInput = [...abortOutputs, ...input];
+ // Build the input list for this turn. When responses are stored on the
+ // server we can simply send the *delta* (the new user input as well as
+ // any pending abort outputs) and rely on `previous_response_id` for
+ // context. When storage is disabled the server has no memory of the
+ // conversation, so we must include the *entire* transcript (minus system
+ // messages) on every call.
+
+ let turnInput: Array = [];
+ // Keeps track of how many items in `turnInput` stem from the existing
+ // transcript so we can avoid re‑emitting them to the UI. Only used when
+ // `disableResponseStorage === true`.
+ let transcriptPrefixLen = 0;
+
+ const stripInternalFields = (
+ item: ResponseInputItem,
+ ): ResponseInputItem => {
+ // Clone shallowly and remove fields that are not part of the public
+ // schema expected by the OpenAI Responses API.
+ // We shallow‑clone the item so that subsequent mutations (deleting
+ // internal fields) do not affect the original object which may still
+ // be referenced elsewhere (e.g. UI components).
+ const clean = { ...item } as Record;
+ delete clean["duration_ms"];
+ // Remove OpenAI-assigned identifiers and transient status so the
+ // backend does not reject items that were never persisted because we
+ // use `store: false`.
+ delete clean["id"];
+ delete clean["status"];
+ return clean as unknown as ResponseInputItem;
+ };
+
+ if (this.disableResponseStorage) {
+ // Remember where the existing transcript ends – everything after this
+ // index in the upcoming `turnInput` list will be *new* for this turn
+ // and therefore needs to be surfaced to the UI.
+ transcriptPrefixLen = this.transcript.length;
+
+ // Ensure the transcript is up‑to‑date with the latest user input so
+ // that subsequent iterations see a complete history.
+ // `turnInput` is still empty at this point (it will be filled later).
+ // We need to look at the *input* items the user just supplied.
+ this.transcript.push(...filterToApiMessages(input));
+
+ turnInput = [...this.transcript, ...abortOutputs].map(
+ stripInternalFields,
+ );
+ } else {
+ turnInput = [...abortOutputs, ...input].map(stripInternalFields);
+ }
this.onLoading(true);
@@ -472,6 +574,51 @@ export class AgentLoop {
this.onItem(item);
// Mark as delivered so flush won't re-emit it
staged[idx] = undefined;
+
+ // When we operate without server‑side storage we keep our own
+ // transcript so we can provide full context on subsequent calls.
+ if (this.disableResponseStorage) {
+ // Exclude system messages from transcript as they do not form
+ // part of the assistant/user dialogue that the model needs.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const role = (item as any).role;
+ if (role !== "system") {
+ // Clone the item to avoid mutating the object that is also
+ // rendered in the UI. We need to strip auxiliary metadata
+ // such as `duration_ms` which is not part of the Responses
+ // API schema and therefore causes a 400 error when included
+ // in subsequent requests whose context is sent verbatim.
+
+ // Skip items that we have already inserted earlier or that the
+ // model does not need to see again in the next turn.
+ // • function_call – superseded by the forthcoming
+ // function_call_output.
+ // • reasoning – internal only, never sent back.
+ // • user messages – we added these to the transcript when
+ // building the first turnInput; stageItem would add a
+ // duplicate.
+ if (
+ (item as ResponseInputItem).type === "function_call" ||
+ (item as ResponseInputItem).type === "reasoning" ||
+ ((item as ResponseInputItem).type === "message" &&
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (item as any).role === "user")
+ ) {
+ return;
+ }
+
+ const clone: ResponseInputItem = {
+ ...(item as unknown as ResponseInputItem),
+ } as ResponseInputItem;
+ // The `duration_ms` field is only added to reasoning items to
+ // show elapsed time in the UI. It must not be forwarded back
+ // to the server.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ delete (clone as any).duration_ms;
+
+ this.transcript.push(clone);
+ }
+ }
}
}, 10);
};
@@ -482,7 +629,22 @@ export class AgentLoop {
return;
}
// send request to openAI
- for (const item of turnInput) {
+ // Only surface the *new* input items to the UI – replaying the entire
+ // transcript would duplicate messages that have already been shown in
+ // earlier turns.
+ // `turnInput` holds the *new* items that will be sent to the API in
+ // this iteration. Surface exactly these to the UI so that we do not
+ // re‑emit messages from previous turns (which would duplicate user
+ // prompts) and so that freshly generated `function_call_output`s are
+ // shown immediately.
+ // Figure out what subset of `turnInput` constitutes *new* information
+ // for the UI so that we don’t spam the interface with repeats of the
+ // entire transcript on every iteration when response storage is
+ // disabled.
+ const deltaInput = this.disableResponseStorage
+ ? turnInput.slice(transcriptPrefixLen)
+ : [...turnInput];
+ for (const item of deltaInput) {
stageItem(item as ResponseItem);
}
// Send request to OpenAI with retry on timeout
@@ -496,52 +658,48 @@ export class AgentLoop {
if (this.model.startsWith("o")) {
reasoning = { effort: "high" };
if (this.model === "o3" || this.model === "o4-mini") {
- // @ts-expect-error waiting for API type update
reasoning.summary = "auto";
}
}
const mergedInstructions = [prefix, this.instructions]
.filter(Boolean)
.join("\n");
- if (isLoggingEnabled()) {
- log(
- `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`,
- );
- }
+
+ const responseCall =
+ !this.config.provider ||
+ this.config.provider?.toLowerCase() === "openai"
+ ? (params: ResponseCreateParams) =>
+ this.oai.responses.create(params)
+ : (params: ResponseCreateParams) =>
+ responsesCreateViaChatCompletions(
+ this.oai,
+ params as ResponseCreateParams & { stream: true },
+ );
+ log(
+ `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`,
+ );
+
// eslint-disable-next-line no-await-in-loop
- stream = await this.oai.responses.create({
+ stream = await responseCall({
model: this.model,
instructions: mergedInstructions,
- previous_response_id: lastResponseId || undefined,
input: turnInput,
stream: true,
parallel_tool_calls: false,
reasoning,
- tools: [
- {
- type: "function",
- name: "shell",
- description: "Runs a shell command, and returns its output.",
- strict: false,
- parameters: {
- type: "object",
- properties: {
- command: { type: "array", items: { type: "string" } },
- workdir: {
- type: "string",
- description: "The working directory for the command.",
- },
- timeout: {
- type: "number",
- description:
- "The maximum time to wait for the command to complete in milliseconds.",
- },
- },
- required: ["command"],
- additionalProperties: false,
- },
- },
- ],
+ ...(this.config.flexMode ? { service_tier: "flex" } : {}),
+ ...(this.disableResponseStorage
+ ? { store: false }
+ : {
+ store: true,
+ previous_response_id: lastResponseId || undefined,
+ }),
+ tools: [shellTool],
+ // Explicitly tell the model it is allowed to pick whatever
+ // tool it deems appropriate. Omitting this sometimes leads to
+ // the model ignoring the available tools and responding with
+ // plain text instead (resulting in a missing tool‑call).
+ tool_choice: "auto",
});
break;
} catch (error) {
@@ -730,71 +888,240 @@ export class AgentLoop {
return;
}
- try {
- // eslint-disable-next-line no-await-in-loop
- for await (const event of stream) {
- if (isLoggingEnabled()) {
+ const MAX_STREAM_RETRIES = 5;
+ let streamRetryAttempt = 0;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ try {
+ // eslint-disable-next-line no-await-in-loop
+ for await (const event of stream as AsyncIterable) {
log(`AgentLoop.run(): response event ${event.type}`);
- }
- // process and surface each item (no‑op until we can depend on streaming events)
- if (event.type === "response.output_item.done") {
- const item = event.item;
- // 1) if it's a reasoning item, annotate it
- type ReasoningItem = { type?: string; duration_ms?: number };
- const maybeReasoning = item as ReasoningItem;
- if (maybeReasoning.type === "reasoning") {
- maybeReasoning.duration_ms = Date.now() - thinkingStart;
- }
- if (item.type === "function_call") {
- // Track outstanding tool call so we can abort later if needed.
- // The item comes from the streaming response, therefore it has
- // either `id` (chat) or `call_id` (responses) – we normalise
- // by reading both.
- const callId =
- (item as { call_id?: string; id?: string }).call_id ??
- (item as { id?: string }).id;
- if (callId) {
- this.pendingAborts.add(callId);
+ // process and surface each item (no-op until we can depend on streaming events)
+ if (event.type === "response.output_item.done") {
+ const item = event.item;
+ // 1) if it's a reasoning item, annotate it
+ type ReasoningItem = { type?: string; duration_ms?: number };
+ const maybeReasoning = item as ReasoningItem;
+ if (maybeReasoning.type === "reasoning") {
+ maybeReasoning.duration_ms = Date.now() - thinkingStart;
}
- } else {
- stageItem(item as ResponseItem);
- }
- }
-
- if (event.type === "response.completed") {
- if (thisGeneration === this.generation && !this.canceled) {
- for (const item of event.response.output) {
+ if (item.type === "function_call") {
+ // Track outstanding tool call so we can abort later if needed.
+ // The item comes from the streaming response, therefore it has
+ // either `id` (chat) or `call_id` (responses) – we normalise
+ // by reading both.
+ const callId =
+ (item as { call_id?: string; id?: string }).call_id ??
+ (item as { id?: string }).id;
+ if (callId) {
+ this.pendingAborts.add(callId);
+ }
+ } else {
stageItem(item as ResponseItem);
}
}
- if (event.response.status === "completed") {
- // TODO: remove this once we can depend on streaming events
- const newTurnInput = await this.processEventsWithoutStreaming(
- event.response.output,
- stageItem,
- );
- turnInput = newTurnInput;
+
+ if (event.type === "response.completed") {
+ if (thisGeneration === this.generation && !this.canceled) {
+ for (const item of event.response.output) {
+ stageItem(item as ResponseItem);
+ }
+ }
+ if (
+ event.response.status === "completed" ||
+ (event.response.status as unknown as string) ===
+ "requires_action"
+ ) {
+ // TODO: remove this once we can depend on streaming events
+ const newTurnInput = await this.processEventsWithoutStreaming(
+ event.response.output,
+ stageItem,
+ );
+
+ // When we do not use server‑side storage we maintain our
+ // own transcript so that *future* turns still contain full
+ // conversational context. However, whether we advance to
+ // another loop iteration should depend solely on the
+ // presence of *new* input items (i.e. items that were not
+ // part of the previous request). Re‑sending the transcript
+ // by itself would create an infinite request loop because
+ // `turnInput.length` would never reach zero.
+
+ if (this.disableResponseStorage) {
+ // 1) Append the freshly emitted output to our local
+ // transcript (minus non‑message items the model does
+ // not need to see again).
+ const cleaned = filterToApiMessages(
+ event.response.output.map(stripInternalFields),
+ );
+ this.transcript.push(...cleaned);
+
+ // 2) Determine the *delta* (newTurnInput) that must be
+ // sent in the next iteration. If there is none we can
+ // safely terminate the loop – the transcript alone
+ // does not constitute new information for the
+ // assistant to act upon.
+
+ const delta = filterToApiMessages(
+ newTurnInput.map(stripInternalFields),
+ );
+
+ if (delta.length === 0) {
+ // No new input => end conversation.
+ turnInput = [];
+ } else {
+ // Re‑send full transcript *plus* the new delta so the
+ // stateless backend receives complete context.
+ turnInput = [...this.transcript, ...delta];
+ // The prefix ends at the current transcript length –
+ // everything after this index is new for the next
+ // iteration.
+ transcriptPrefixLen = this.transcript.length;
+ }
+ } else {
+ turnInput = newTurnInput;
+ }
+ }
+ lastResponseId = event.response.id;
+ this.onLastResponseId(event.response.id);
}
- lastResponseId = event.response.id;
- this.onLastResponseId(event.response.id);
}
- }
- } catch (err: unknown) {
- // Gracefully handle an abort triggered via `cancel()` so that the
- // consumer does not see an unhandled exception.
- if (err instanceof Error && err.name === "AbortError") {
- if (!this.canceled) {
- // It was aborted for some other reason; surface the error.
- throw err;
+ // Stream finished successfully – leave the retry loop.
+ break;
+ } catch (err: unknown) {
+ const isRateLimitError = (e: unknown): boolean => {
+ if (!e || typeof e !== "object") {
+ return false;
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const ex: any = e;
+ return (
+ ex.status === 429 ||
+ ex.code === "rate_limit_exceeded" ||
+ ex.type === "rate_limit_exceeded"
+ );
+ };
+
+ if (
+ isRateLimitError(err) &&
+ streamRetryAttempt < MAX_STREAM_RETRIES
+ ) {
+ streamRetryAttempt += 1;
+
+ const waitMs =
+ RATE_LIMIT_RETRY_WAIT_MS * 2 ** (streamRetryAttempt - 1);
+ log(
+ `OpenAI stream rate‑limited – retry ${streamRetryAttempt}/${MAX_STREAM_RETRIES} in ${waitMs} ms`,
+ );
+
+ // Give the server a breather before retrying.
+ // eslint-disable-next-line no-await-in-loop
+ await new Promise((res) => setTimeout(res, waitMs));
+
+ // Re‑create the stream with the *same* parameters.
+ let reasoning: Reasoning | undefined;
+ if (this.model.startsWith("o")) {
+ reasoning = { effort: "high" };
+ if (this.model === "o3" || this.model === "o4-mini") {
+ reasoning.summary = "auto";
+ }
+ }
+
+ const mergedInstructions = [prefix, this.instructions]
+ .filter(Boolean)
+ .join("\n");
+
+ const responseCall =
+ !this.config.provider ||
+ this.config.provider?.toLowerCase() === "openai"
+ ? (params: ResponseCreateParams) =>
+ this.oai.responses.create(params)
+ : (params: ResponseCreateParams) =>
+ responsesCreateViaChatCompletions(
+ this.oai,
+ params as ResponseCreateParams & { stream: true },
+ );
+
+ log(
+ "agentLoop.run(): responseCall(1): turnInput: " +
+ JSON.stringify(turnInput),
+ );
+ // eslint-disable-next-line no-await-in-loop
+ stream = await responseCall({
+ model: this.model,
+ instructions: mergedInstructions,
+ input: turnInput,
+ stream: true,
+ parallel_tool_calls: false,
+ reasoning,
+ ...(this.config.flexMode ? { service_tier: "flex" } : {}),
+ ...(this.disableResponseStorage
+ ? { store: false }
+ : {
+ store: true,
+ previous_response_id: lastResponseId || undefined,
+ }),
+ tools: [shellTool],
+ tool_choice: "auto",
+ });
+
+ this.currentStream = stream;
+ // Continue to outer while to consume new stream.
+ continue;
}
- this.onLoading(false);
- return;
+
+ // Gracefully handle an abort triggered via `cancel()` so that the
+ // consumer does not see an unhandled exception.
+ if (err instanceof Error && err.name === "AbortError") {
+ if (!this.canceled) {
+ // It was aborted for some other reason; surface the error.
+ throw err;
+ }
+ this.onLoading(false);
+ return;
+ }
+ // Suppress internal stack on JSON parse failures
+ if (err instanceof SyntaxError) {
+ this.onItem({
+ id: `error-${Date.now()}`,
+ type: "message",
+ role: "system",
+ content: [
+ {
+ type: "input_text",
+ text: "⚠️ Failed to parse streaming response (invalid JSON). Please `/clear` to reset.",
+ },
+ ],
+ });
+ this.onLoading(false);
+ return;
+ }
+ // Handle OpenAI API quota errors
+ if (
+ err instanceof Error &&
+ (err as { code?: string }).code === "insufficient_quota"
+ ) {
+ this.onItem({
+ id: `error-${Date.now()}`,
+ type: "message",
+ role: "system",
+ content: [
+ {
+ type: "input_text",
+ text: "⚠️ Insufficient quota. Please check your billing details and retry.",
+ },
+ ],
+ });
+ this.onLoading(false);
+ return;
+ }
+ throw err;
+ } finally {
+ this.currentStream = null;
}
- throw err;
- } finally {
- this.currentStream = null;
- }
+ } // end while retry loop
log(
`Turn inputs (${turnInput.length}) - ${turnInput
@@ -894,7 +1221,7 @@ export class AgentLoop {
],
});
} catch {
- /* no‑op – emitting the error message is best‑effort */
+ /* no-op – emitting the error message is best‑effort */
}
this.onLoading(false);
return;
@@ -1145,7 +1472,21 @@ You MUST adhere to the following criteria when executing the task:
- For smaller tasks, describe in brief bullet points
- For more complex tasks, include brief high-level description, use bullet points, and include details that would be relevant to a code reviewer.
- If completing the user's task DOES NOT require writing or modifying files (e.g., the user asks a question about the code base):
- - Respond in a friendly tune as a remote teammate, who is knowledgeable, capable and eager to help with coding.
+ - Respond in a friendly tone as a remote teammate, who is knowledgeable, capable and eager to help with coding.
- When your task involves writing or modifying files:
- Do NOT tell the user to "save the file" or "copy the code into a file" if you already created or modified the file using \`apply_patch\`. Instead, reference the file as already saved.
- Do NOT show the full contents of large files you have already written, unless the user explicitly asks for them.`;
+
+function filterToApiMessages(
+ items: Array,
+): Array {
+ return items.filter((it) => {
+ if (it.type === "message" && it.role === "system") {
+ return false;
+ }
+ if (it.type === "reasoning") {
+ return false;
+ }
+ return true;
+ });
+}
diff --git a/codex-cli/src/utils/agent/exec.ts b/codex-cli/src/utils/agent/exec.ts
index 22b75c0276..9c763ef551 100644
--- a/codex-cli/src/utils/agent/exec.ts
+++ b/codex-cli/src/utils/agent/exec.ts
@@ -1,5 +1,6 @@
import type { ExecInput, ExecResult } from "./sandbox/interface.js";
import type { SpawnOptions } from "child_process";
+import type { ParseEntry } from "shell-quote";
import { process_patch } from "./apply-patch.js";
import { SandboxType } from "./sandbox/interface.js";
@@ -8,9 +9,25 @@ import { exec as rawExec } from "./sandbox/raw-exec.js";
import { formatCommandForDisplay } from "../../format-command.js";
import fs from "fs";
import os from "os";
+import path from "path";
+import { parse } from "shell-quote";
+import { resolvePathAgainstWorkdir } from "src/approvals.js";
const DEFAULT_TIMEOUT_MS = 10_000; // 10 seconds
+function requiresShell(cmd: Array): boolean {
+ // If the command is a single string that contains shell operators,
+ // it needs to be run with shell: true
+ if (cmd.length === 1 && cmd[0] !== undefined) {
+ const tokens = parse(cmd[0]) as Array;
+ return tokens.some((token) => typeof token === "object" && "op" in token);
+ }
+
+ // If the command is split into multiple arguments, we don't need shell: true
+ // even if one of the arguments is a shell operator like '|'
+ return false;
+}
+
/**
* This function should never return a rejected promise: errors should be
* mapped to a non-zero exit code and the error message should be in stderr.
@@ -33,6 +50,7 @@ export function exec(
const opts: SpawnOptions = {
timeout: timeoutInMillis || DEFAULT_TIMEOUT_MS,
+ ...(requiresShell(cmd) ? { shell: true } : {}),
...(workdir ? { cwd: workdir } : {}),
};
// Merge default writable roots with any user-specified ones.
@@ -44,16 +62,32 @@ export function exec(
return execForSandbox(cmd, opts, writableRoots, abortSignal);
}
-export function execApplyPatch(patchText: string): ExecResult {
+export function execApplyPatch(
+ patchText: string,
+ workdir: string | undefined = undefined,
+): ExecResult {
// This is a temporary measure to understand what are the common base commands
// until we start persisting and uploading rollouts
try {
const result = process_patch(
patchText,
- (p) => fs.readFileSync(p, "utf8"),
- (p, c) => fs.writeFileSync(p, c, "utf8"),
- (p) => fs.unlinkSync(p),
+ (p) => fs.readFileSync(resolvePathAgainstWorkdir(p, workdir), "utf8"),
+ (p, c) => {
+ const resolvedPath = resolvePathAgainstWorkdir(p, workdir);
+
+ // Ensure the parent directory exists before writing the file. This
+ // mirrors the behaviour of the standalone apply_patch CLI (see
+ // write_file() in apply-patch.ts) and prevents errors when adding a
+ // new file in a not‑yet‑created sub‑directory.
+ const dir = path.dirname(resolvedPath);
+ if (dir !== ".") {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+
+ fs.writeFileSync(resolvedPath, c, "utf8");
+ },
+ (p) => fs.unlinkSync(resolvePathAgainstWorkdir(p, workdir)),
);
return {
stdout: result,
diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts
index 1af390e226..85d6869192 100644
--- a/codex-cli/src/utils/agent/handle-exec-command.ts
+++ b/codex-cli/src/utils/agent/handle-exec-command.ts
@@ -5,12 +5,12 @@ import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js";
import type { ResponseInputItem } from "openai/resources/responses/responses.mjs";
import { exec, execApplyPatch } from "./exec.js";
-import { isLoggingEnabled, log } from "./log.js";
import { ReviewDecision } from "./review.js";
import { FullAutoErrorMode } from "../auto-approval-mode.js";
import { SandboxType } from "./sandbox/interface.js";
import { canAutoApprove } from "../../approvals.js";
import { formatCommandForDisplay } from "../../format-command.js";
+import { isLoggingEnabled, log } from "../logger/log.js";
import { access } from "fs/promises";
// ---------------------------------------------------------------------------
@@ -81,7 +81,7 @@ export async function handleExecCommand(
) => Promise,
abortSignal?: AbortSignal,
): Promise {
- const { cmd: command } = args;
+ const { cmd: command, workdir } = args;
const key = deriveCommandKey(command);
@@ -103,7 +103,7 @@ export async function handleExecCommand(
// working directory so that edits are constrained to the project root. If
// the caller wishes to broaden or restrict the set it can be made
// configurable in the future.
- const safety = canAutoApprove(command, policy, [process.cwd()]);
+ const safety = canAutoApprove(command, workdir, policy, [process.cwd()]);
let runInSandbox: boolean;
switch (safety.type) {
@@ -144,7 +144,7 @@ export async function handleExecCommand(
abortSignal,
);
// If the operation was aborted in the meantime, propagate the cancellation
- // upward by returning an empty (no‑op) result so that the agent loop will
+ // upward by returning an empty (no-op) result so that the agent loop will
// exit cleanly without emitting spurious output.
if (abortSignal?.aborted) {
return {
@@ -223,23 +223,22 @@ async function execCommand(
workdir = process.cwd();
}
}
- if (isLoggingEnabled()) {
- if (applyPatchCommand != null) {
- log("EXEC running apply_patch command");
- } else {
- const { cmd, timeoutInMillis } = execInput;
- // Seconds are a bit easier to read in log messages and most timeouts
- // are specified as multiples of 1000, anyway.
- const timeout =
- timeoutInMillis != null
- ? Math.round(timeoutInMillis / 1000).toString()
- : "undefined";
- log(
- `EXEC running \`${formatCommandForDisplay(
- cmd,
- )}\` in workdir=${workdir} with timeout=${timeout}s`,
- );
- }
+
+ if (applyPatchCommand != null) {
+ log("EXEC running apply_patch command");
+ } else if (isLoggingEnabled()) {
+ const { cmd, timeoutInMillis } = execInput;
+ // Seconds are a bit easier to read in log messages and most timeouts
+ // are specified as multiples of 1000, anyway.
+ const timeout =
+ timeoutInMillis != null
+ ? Math.round(timeoutInMillis / 1000).toString()
+ : "undefined";
+ log(
+ `EXEC running \`${formatCommandForDisplay(
+ cmd,
+ )}\` in workdir=${workdir} with timeout=${timeout}s`,
+ );
}
// Note execApplyPatch() and exec() are coded defensively and should not
@@ -248,7 +247,7 @@ async function execCommand(
const start = Date.now();
const execResult =
applyPatchCommand != null
- ? execApplyPatch(applyPatchCommand.patch)
+ ? execApplyPatch(applyPatchCommand.patch, workdir)
: await exec(
{ ...execInput, additionalWritableRoots },
await getSandbox(runInSandbox),
diff --git a/codex-cli/src/utils/agent/platform-commands.ts b/codex-cli/src/utils/agent/platform-commands.ts
index 7be02c7ac5..161388f942 100644
--- a/codex-cli/src/utils/agent/platform-commands.ts
+++ b/codex-cli/src/utils/agent/platform-commands.ts
@@ -2,7 +2,7 @@
* Utility functions for handling platform-specific commands
*/
-import { log, isLoggingEnabled } from "./log.js";
+import { log } from "../logger/log.js";
/**
* Map of Unix commands to their Windows equivalents
@@ -59,9 +59,7 @@ export function adaptCommandForPlatform(command: Array): Array {
return command;
}
- if (isLoggingEnabled()) {
- log(`Adapting command '${cmd}' for Windows platform`);
- }
+ log(`Adapting command '${cmd}' for Windows platform`);
// Create a new command array with the adapted command
const adaptedCommand = [...command];
@@ -78,9 +76,7 @@ export function adaptCommandForPlatform(command: Array): Array {
}
}
- if (isLoggingEnabled()) {
- log(`Adapted command: ${adaptedCommand.join(" ")}`);
- }
+ log(`Adapted command: ${adaptedCommand.join(" ")}`);
return adaptedCommand;
}
diff --git a/codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts b/codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts
new file mode 100644
index 0000000000..518d475c78
--- /dev/null
+++ b/codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts
@@ -0,0 +1,78 @@
+// Maximum output cap: either MAX_OUTPUT_LINES lines or MAX_OUTPUT_BYTES bytes,
+// whichever limit is reached first.
+const MAX_OUTPUT_BYTES = 1024 * 10; // 10 KB
+const MAX_OUTPUT_LINES = 256;
+
+/**
+ * Creates a collector that accumulates data Buffers from a stream up to
+ * specified byte and line limits. After either limit is exceeded, further
+ * data is ignored.
+ */
+export function createTruncatingCollector(
+ stream: NodeJS.ReadableStream,
+ byteLimit: number = MAX_OUTPUT_BYTES,
+ lineLimit: number = MAX_OUTPUT_LINES,
+): {
+ getString: () => string;
+ hit: boolean;
+} {
+ const chunks: Array = [];
+ let totalBytes = 0;
+ let totalLines = 0;
+ let hitLimit = false;
+
+ stream?.on("data", (data: Buffer) => {
+ if (hitLimit) {
+ return;
+ }
+ const dataLength = data.length;
+ let newlineCount = 0;
+ for (let i = 0; i < dataLength; i++) {
+ if (data[i] === 0x0a) {
+ newlineCount++;
+ }
+ }
+ // If entire chunk fits within byte and line limits, take it whole
+ if (
+ totalBytes + dataLength <= byteLimit &&
+ totalLines + newlineCount <= lineLimit
+ ) {
+ chunks.push(data);
+ totalBytes += dataLength;
+ totalLines += newlineCount;
+ } else {
+ // Otherwise, take a partial slice up to the first limit breach
+ const allowedBytes = byteLimit - totalBytes;
+ const allowedLines = lineLimit - totalLines;
+ let bytesTaken = 0;
+ let linesSeen = 0;
+ for (let i = 0; i < dataLength; i++) {
+ // Stop if byte or line limit is reached
+ if (bytesTaken === allowedBytes || linesSeen === allowedLines) {
+ break;
+ }
+ const byte = data[i];
+ if (byte === 0x0a) {
+ linesSeen++;
+ }
+ bytesTaken++;
+ }
+ if (bytesTaken > 0) {
+ chunks.push(data.slice(0, bytesTaken));
+ totalBytes += bytesTaken;
+ totalLines += linesSeen;
+ }
+ hitLimit = true;
+ }
+ });
+
+ return {
+ getString() {
+ return Buffer.concat(chunks).toString("utf8");
+ },
+ /** True if either byte or line limit was exceeded */
+ get hit(): boolean {
+ return hitLimit;
+ },
+ };
+}
diff --git a/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts b/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts
index 77f7216fd4..934056d9af 100644
--- a/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts
+++ b/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts
@@ -2,13 +2,10 @@ import type { ExecResult } from "./interface.js";
import type { SpawnOptions } from "child_process";
import { exec } from "./raw-exec.js";
-import { log } from "../log.js";
-import { realpathSync } from "fs";
-import { CONFIG_DIR } from "src/utils/config.js";
+import { log } from "../../logger/log.js";
function getCommonRoots() {
return [
- CONFIG_DIR,
// Without this root, it'll cause:
// pyenv: cannot rehash: $HOME/.pyenv/shims isn't writable
`${process.env["HOME"]}/.pyenv`,
@@ -18,21 +15,20 @@ function getCommonRoots() {
export function execWithSeatbelt(
cmd: Array,
opts: SpawnOptions,
- writableRoots: Array,
+ writableRoots: ReadonlyArray,
abortSignal?: AbortSignal,
): Promise {
let scopedWritePolicy: string;
let policyTemplateParams: Array;
- if (writableRoots.length > 0) {
- // Add `~/.codex` to the list of writable roots
- // (if there's any already, not in read-only mode)
- getCommonRoots().map((root) => writableRoots.push(root));
- const { policies, params } = writableRoots
+
+ const fullWritableRoots = [...writableRoots, ...getCommonRoots()];
+ // In practice, fullWritableRoots will be non-empty, but we check just in
+ // case the logic to build up fullWritableRoots changes.
+ if (fullWritableRoots.length > 0) {
+ const { policies, params } = fullWritableRoots
.map((root, index) => ({
policy: `(subpath (param "WRITABLE_ROOT_${index}"))`,
- // the kernel resolves symlinks before handing them to seatbelt for checking
- // so store the canonicalized form in the policy to be compared against
- param: `-DWRITABLE_ROOT_${index}=${realpathSync(root)}`,
+ param: `-DWRITABLE_ROOT_${index}=${root}`,
}))
.reduce(
(
diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts
index 6cfb304731..b33feb8518 100644
--- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts
+++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts
@@ -7,13 +7,12 @@ import type {
StdioPipe,
} from "child_process";
-import { log, isLoggingEnabled } from "../log.js";
+import { log } from "../../logger/log.js";
import { adaptCommandForPlatform } from "../platform-commands.js";
+import { createTruncatingCollector } from "./create-truncating-collector";
import { spawn } from "child_process";
import * as os from "os";
-const MAX_BUFFER = 1024 * 100; // 100 KB
-
/**
* This function should never return a rejected promise: errors should be
* mapped to a non-zero exit code and the error message should be in stderr.
@@ -21,16 +20,13 @@ const MAX_BUFFER = 1024 * 100; // 100 KB
export function exec(
command: Array,
options: SpawnOptions,
- _writableRoots: Array,
+ _writableRoots: ReadonlyArray,
abortSignal?: AbortSignal,
): Promise {
// Adapt command for the current platform (e.g., convert 'ls' to 'dir' on Windows)
const adaptedCommand = adaptCommandForPlatform(command);
- if (
- isLoggingEnabled() &&
- JSON.stringify(adaptedCommand) !== JSON.stringify(command)
- ) {
+ if (JSON.stringify(adaptedCommand) !== JSON.stringify(command)) {
log(
`Command adapted for platform: ${command.join(
" ",
@@ -95,9 +91,7 @@ export function exec(
// timely fashion.
if (abortSignal) {
const abortHandler = () => {
- if (isLoggingEnabled()) {
- log(`raw-exec: abort signal received – killing child ${child.pid}`);
- }
+ log(`raw-exec: abort signal received – killing child ${child.pid}`);
const killTarget = (signal: NodeJS.Signals) => {
if (!child.pid) {
return;
@@ -148,37 +142,14 @@ export function exec(
// resolve the promise and translate the failure into a regular
// ExecResult object so the rest of the agent loop can carry on gracefully.
- const stdoutChunks: Array = [];
- const stderrChunks: Array = [];
- let numStdoutBytes = 0;
- let numStderrBytes = 0;
- let hitMaxStdout = false;
- let hitMaxStderr = false;
-
return new Promise((resolve) => {
- child.stdout?.on("data", (data: Buffer) => {
- if (!hitMaxStdout) {
- numStdoutBytes += data.length;
- if (numStdoutBytes <= MAX_BUFFER) {
- stdoutChunks.push(data);
- } else {
- hitMaxStdout = true;
- }
- }
- });
- child.stderr?.on("data", (data: Buffer) => {
- if (!hitMaxStderr) {
- numStderrBytes += data.length;
- if (numStderrBytes <= MAX_BUFFER) {
- stderrChunks.push(data);
- } else {
- hitMaxStderr = true;
- }
- }
- });
+ // Collect stdout and stderr up to configured limits.
+ const stdoutCollector = createTruncatingCollector(child.stdout!);
+ const stderrCollector = createTruncatingCollector(child.stderr!);
+
child.on("exit", (code, signal) => {
- const stdout = Buffer.concat(stdoutChunks).toString("utf8");
- const stderr = Buffer.concat(stderrChunks).toString("utf8");
+ const stdout = stdoutCollector.getString();
+ const stderr = stderrCollector.getString();
// Map (code, signal) to an exit code. We expect exactly one of the two
// values to be non-null, but we code defensively to handle the case where
@@ -194,24 +165,61 @@ export function exec(
exitCode = 1;
}
- if (isLoggingEnabled()) {
- log(
- `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`,
- );
- }
- resolve({
+ log(
+ `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`,
+ );
+
+ const execResult = {
stdout,
stderr,
exitCode,
- });
+ };
+ resolve(
+ addTruncationWarningsIfNecessary(
+ execResult,
+ stdoutCollector.hit,
+ stderrCollector.hit,
+ ),
+ );
});
child.on("error", (err) => {
- resolve({
+ const execResult = {
stdout: "",
stderr: String(err),
exitCode: 1,
- });
+ };
+ resolve(
+ addTruncationWarningsIfNecessary(
+ execResult,
+ stdoutCollector.hit,
+ stderrCollector.hit,
+ ),
+ );
});
});
}
+
+/**
+ * Adds a truncation warnings to stdout and stderr, if appropriate.
+ */
+function addTruncationWarningsIfNecessary(
+ execResult: ExecResult,
+ hitMaxStdout: boolean,
+ hitMaxStderr: boolean,
+): ExecResult {
+ if (!hitMaxStdout && !hitMaxStderr) {
+ return execResult;
+ } else {
+ const { stdout, stderr, exitCode } = execResult;
+ return {
+ stdout: hitMaxStdout
+ ? stdout + "\n\n[Output truncated: too many lines or bytes]"
+ : stdout,
+ stderr: hitMaxStderr
+ ? stderr + "\n\n[Output truncated: too many lines or bytes]"
+ : stderr,
+ exitCode,
+ };
+ }
+}
diff --git a/codex-cli/src/utils/approximate-tokens-used.ts b/codex-cli/src/utils/approximate-tokens-used.ts
index a50dd7305c..569ccebd1d 100644
--- a/codex-cli/src/utils/approximate-tokens-used.ts
+++ b/codex-cli/src/utils/approximate-tokens-used.ts
@@ -19,6 +19,10 @@ export function approximateTokensUsed(items: Array): number {
for (const item of items) {
switch (item.type) {
case "message": {
+ if (item.role !== "user" && item.role !== "assistant") {
+ continue;
+ }
+
for (const c of item.content) {
if (c.type === "input_text" || c.type === "output_text") {
charCount += c.text.length;
diff --git a/codex-cli/src/utils/bug-report.ts b/codex-cli/src/utils/bug-report.ts
new file mode 100644
index 0000000000..768c695d5b
--- /dev/null
+++ b/codex-cli/src/utils/bug-report.ts
@@ -0,0 +1,82 @@
+import type {
+ ResponseItem,
+ ResponseOutputItem,
+} from "openai/resources/responses/responses.mjs";
+
+/**
+ * Build a GitHub issues‐new URL that pre‑fills the Codex 2‑bug‑report.yml
+ * template with whatever structured data we can infer from the current
+ * session.
+ */
+export function buildBugReportUrl({
+ items,
+ cliVersion,
+ model,
+ platform,
+}: {
+ /** Chat history so we can summarise user steps */
+ items: Array;
+ /** CLI revision string (e.g. output of `codex --revision`) */
+ cliVersion: string;
+ /** Active model name */
+ model: string;
+ /** Platform string – e.g. `darwin arm64 23.0.0` */
+ platform: string;
+}): string {
+ const params = new URLSearchParams({
+ template: "2-bug-report.yml",
+ labels: "bug",
+ });
+
+ params.set("version", cliVersion);
+ params.set("model", model);
+ params.set("platform", platform);
+
+ const bullets: Array = [];
+ for (let i = 0; i < items.length; ) {
+ const entry = items[i];
+ if (entry?.type === "message" && entry.role === "user") {
+ const contentArray = entry.content as
+ | Array<{ text?: string }>
+ | undefined;
+ const messageText = contentArray
+ ?.map((c) => c.text ?? "")
+ .join(" ")
+ .trim();
+
+ let reasoning = 0;
+ let toolCalls = 0;
+ let j = i + 1;
+ while (j < items.length) {
+ const it = items[j];
+ if (it?.type === "message" && it?.role === "user") {
+ break;
+ } else if (
+ it?.type === "reasoning" ||
+ (it?.type === "message" && it?.role === "assistant")
+ ) {
+ reasoning += 1;
+ } else if (it?.type === "function_call") {
+ toolCalls += 1;
+ }
+ j++;
+ }
+
+ const codeBlock = `\`\`\`\n ${messageText}\n \`\`\``;
+
+ bullets.push(
+ `- ${codeBlock}\n - \`${reasoning} reasoning\` | \`${toolCalls} tool\``,
+ );
+
+ i = j;
+ } else {
+ i += 1;
+ }
+ }
+
+ if (bullets.length) {
+ params.set("steps", bullets.join("\n"));
+ }
+
+ return `https://github.com/openai/codex/issues/new?${params.toString()}`;
+}
diff --git a/codex-cli/src/utils/check-updates.ts b/codex-cli/src/utils/check-updates.ts
new file mode 100644
index 0000000000..5e326c1c93
--- /dev/null
+++ b/codex-cli/src/utils/check-updates.ts
@@ -0,0 +1,146 @@
+import type { AgentName } from "package-manager-detector";
+
+import { detectInstallerByPath } from "./package-manager-detector";
+import { CLI_VERSION } from "./session";
+import boxen from "boxen";
+import chalk from "chalk";
+import { getLatestVersion } from "fast-npm-meta";
+import { readFile, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { getUserAgent } from "package-manager-detector";
+import semver from "semver";
+
+interface UpdateCheckState {
+ lastUpdateCheck?: string;
+}
+
+interface UpdateCheckInfo {
+ currentVersion: string;
+ latestVersion: string;
+}
+
+export interface UpdateOptions {
+ manager: AgentName;
+ packageName: string;
+}
+
+const UPDATE_CHECK_FREQUENCY = 1000 * 60 * 60 * 24; // 1 day
+
+export function renderUpdateCommand({
+ manager,
+ packageName,
+}: UpdateOptions): string {
+ const updateCommands: Record = {
+ npm: `npm install -g ${packageName}`,
+ pnpm: `pnpm add -g ${packageName}`,
+ bun: `bun add -g ${packageName}`,
+ /** Only works in yarn@v1 */
+ yarn: `yarn global add ${packageName}`,
+ deno: `deno install -g npm:${packageName}`,
+ };
+
+ return updateCommands[manager];
+}
+
+function renderUpdateMessage(options: UpdateOptions) {
+ const updateCommand = renderUpdateCommand(options);
+ return `To update, run ${chalk.magenta(updateCommand)} to update.`;
+}
+
+async function writeState(stateFilePath: string, state: UpdateCheckState) {
+ await writeFile(stateFilePath, JSON.stringify(state, null, 2), {
+ encoding: "utf8",
+ });
+}
+
+async function getUpdateCheckInfo(
+ packageName: string,
+): Promise {
+ const metadata = await getLatestVersion(packageName, {
+ force: true,
+ throw: false,
+ });
+
+ if ("error" in metadata || !metadata?.version) {
+ return;
+ }
+
+ return {
+ currentVersion: CLI_VERSION,
+ latestVersion: metadata.version,
+ };
+}
+
+export async function checkForUpdates(): Promise {
+ const { CONFIG_DIR } = await import("./config");
+ const stateFile = join(CONFIG_DIR, "update-check.json");
+
+ // Load previous check timestamp
+ let state: UpdateCheckState | undefined;
+ try {
+ state = JSON.parse(await readFile(stateFile, "utf8"));
+ } catch {
+ // ignore
+ }
+
+ // Bail out if we checked less than the configured frequency ago
+ if (
+ state?.lastUpdateCheck &&
+ Date.now() - new Date(state.lastUpdateCheck).valueOf() <
+ UPDATE_CHECK_FREQUENCY
+ ) {
+ return;
+ }
+
+ // Fetch current vs latest from the registry
+ const { name: packageName } = await import("../../package.json");
+ const packageInfo = await getUpdateCheckInfo(packageName);
+
+ await writeState(stateFile, {
+ ...state,
+ lastUpdateCheck: new Date().toUTCString(),
+ });
+
+ if (
+ !packageInfo ||
+ !semver.gt(packageInfo.latestVersion, packageInfo.currentVersion)
+ ) {
+ return;
+ }
+
+ // Detect global installer
+ let managerName = await detectInstallerByPath();
+
+ // Fallback to the local package manager
+ if (!managerName) {
+ const local = getUserAgent();
+ if (!local) {
+ // No package managers found, skip it.
+ return;
+ }
+ managerName = local;
+ }
+
+ const updateMessage = renderUpdateMessage({
+ manager: managerName,
+ packageName,
+ });
+
+ const box = boxen(
+ `\
+Update available! ${chalk.red(packageInfo.currentVersion)} → ${chalk.green(
+ packageInfo.latestVersion,
+ )}.
+${updateMessage}`,
+ {
+ padding: 1,
+ margin: 1,
+ align: "center",
+ borderColor: "yellow",
+ borderStyle: "round",
+ },
+ );
+
+ // eslint-disable-next-line no-console
+ console.log(box);
+}
diff --git a/codex-cli/src/utils/compact-summary.ts b/codex-cli/src/utils/compact-summary.ts
index 81474396cb..82a337e385 100644
--- a/codex-cli/src/utils/compact-summary.ts
+++ b/codex-cli/src/utils/compact-summary.ts
@@ -1,21 +1,31 @@
+import type { AppConfig } from "./config.js";
import type { ResponseItem } from "openai/resources/responses/responses.mjs";
-import { OPENAI_BASE_URL } from "./config.js";
+import { getBaseUrl, getApiKey } from "./config.js";
import OpenAI from "openai";
-
/**
* Generate a condensed summary of the conversation items.
* @param items The list of conversation items to summarize
* @param model The model to use for generating the summary
* @returns A concise structured summary string
*/
+/**
+ * Generate a condensed summary of the conversation items.
+ * @param items The list of conversation items to summarize
+ * @param model The model to use for generating the summary
+ * @param flexMode Whether to use the flex-mode service tier
+ * @param config The configuration object
+ * @returns A concise structured summary string
+ */
export async function generateCompactSummary(
items: Array,
model: string,
+ flexMode = false,
+ config: AppConfig,
): Promise {
const oai = new OpenAI({
- apiKey: process.env["OPENAI_API_KEY"],
- baseURL: OPENAI_BASE_URL,
+ apiKey: getApiKey(config.provider),
+ baseURL: getBaseUrl(config.provider),
});
const conversationText = items
@@ -44,6 +54,7 @@ export async function generateCompactSummary(
const response = await oai.chat.completions.create({
model,
+ ...(flexMode ? { service_tier: "flex" } : {}),
messages: [
{
role: "assistant",
diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts
index 309256e9b1..1df2e197c7 100644
--- a/codex-cli/src/utils/config.ts
+++ b/codex-cli/src/utils/config.ts
@@ -8,8 +8,9 @@
import type { FullAutoErrorMode } from "./auto-approval-mode.js";
-import { log, isLoggingEnabled } from "./agent/log.js";
import { AutoApprovalMode } from "./auto-approval-mode.js";
+import { log } from "./logger/log.js";
+import { providers } from "./providers.js";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { load as loadYaml, dump as dumpYaml } from "js-yaml";
import { homedir } from "os";
@@ -40,17 +41,62 @@ export function setApiKey(apiKey: string): void {
OPENAI_API_KEY = apiKey;
}
-// Formatting (quiet mode-only).
-export const PRETTY_PRINT = Boolean(process.env["PRETTY_PRINT"] || "");
+export function getBaseUrl(provider: string = "openai"): string | undefined {
+ // Check for a PROVIDER-specific override: e.g. OPENAI_BASE_URL or OLLAMA_BASE_URL.
+ const envKey = `${provider.toUpperCase()}_BASE_URL`;
+ if (process.env[envKey]) {
+ return process.env[envKey];
+ }
+
+ // Get providers config from config file.
+ const config = loadConfig();
+ const providersConfig = config.providers ?? providers;
+ const providerInfo = providersConfig[provider.toLowerCase()];
+ if (providerInfo) {
+ return providerInfo.baseURL;
+ }
+
+ // If the provider not found in the providers list and `OPENAI_BASE_URL` is set, use it.
+ if (OPENAI_BASE_URL !== "") {
+ return OPENAI_BASE_URL;
+ }
+
+ // We tried.
+ return undefined;
+}
+
+export function getApiKey(provider: string = "openai"): string | undefined {
+ const config = loadConfig();
+ const providersConfig = config.providers ?? providers;
+ const providerInfo = providersConfig[provider.toLowerCase()];
+ if (providerInfo) {
+ if (providerInfo.name === "Ollama") {
+ return process.env[providerInfo.envKey] ?? "dummy";
+ }
+ return process.env[providerInfo.envKey];
+ }
+
+ // If the provider not found in the providers list and `OPENAI_API_KEY` is set, use it
+ if (OPENAI_API_KEY !== "") {
+ return OPENAI_API_KEY;
+ }
+
+ // We tried.
+ return undefined;
+}
// Represents config as persisted in config.json.
export type StoredConfig = {
model?: string;
+ provider?: string;
approvalMode?: AutoApprovalMode;
fullAutoErrorMode?: FullAutoErrorMode;
memory?: MemoryConfig;
/** Whether to enable desktop notifications for responses */
notify?: boolean;
+ /** Disable server-side response storage (send full transcript each request) */
+ disableResponseStorage?: boolean;
+ providers?: Record;
history?: {
maxSize?: number;
saveHistory?: boolean;
@@ -74,11 +120,20 @@ export type MemoryConfig = {
export type AppConfig = {
apiKey?: string;
model: string;
+ provider?: string;
instructions: string;
+ approvalMode?: AutoApprovalMode;
fullAutoErrorMode?: FullAutoErrorMode;
memory?: MemoryConfig;
/** Whether to enable desktop notifications for responses */
notify: boolean;
+
+ /** Disable server-side response storage (send full transcript each request) */
+ disableResponseStorage?: boolean;
+
+ /** Enable the "flex-mode" processing mode for supported models (o3, o4-mini) */
+ flexMode?: boolean;
+ providers?: Record;
history?: {
maxSize: number;
saveHistory: boolean;
@@ -86,6 +141,9 @@ export type AppConfig = {
};
};
+// Formatting (quiet mode-only).
+export const PRETTY_PRINT = Boolean(process.env["PRETTY_PRINT"] || "");
+
// ---------------------------------------------------------------------------
// Project doc support (codex.md)
// ---------------------------------------------------------------------------
@@ -237,15 +295,11 @@ export const loadConfig = (
? resolvePath(cwd, options.projectDocPath)
: discoverProjectDocPath(cwd);
if (projectDocPath) {
- if (isLoggingEnabled()) {
- log(
- `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`,
- );
- }
+ log(
+ `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`,
+ );
} else {
- if (isLoggingEnabled()) {
- log(`[codex] No project doc found in ${cwd}`);
- }
+ log(`[codex] No project doc found in ${cwd}`);
}
}
@@ -266,8 +320,11 @@ export const loadConfig = (
(options.isFullContext
? DEFAULT_FULL_CONTEXT_MODEL
: DEFAULT_AGENTIC_MODEL),
+ provider: storedConfig.provider,
instructions: combinedInstructions,
notify: storedConfig.notify === true,
+ approvalMode: storedConfig.approvalMode,
+ disableResponseStorage: storedConfig.disableResponseStorage ?? false,
};
// -----------------------------------------------------------------------
@@ -345,6 +402,9 @@ export const loadConfig = (
};
}
+ // Merge default providers with user configured providers in the config.
+ config.providers = { ...providers, ...storedConfig.providers };
+
return config;
};
@@ -376,6 +436,9 @@ export const saveConfig = (
// Create the config object to save
const configToSave: StoredConfig = {
model: config.model,
+ provider: config.provider,
+ providers: config.providers,
+ approvalMode: config.approvalMode,
};
// Add history settings if they exist
diff --git a/codex-cli/src/utils/extract-applied-patches.ts b/codex-cli/src/utils/extract-applied-patches.ts
new file mode 100644
index 0000000000..3e9bc10479
--- /dev/null
+++ b/codex-cli/src/utils/extract-applied-patches.ts
@@ -0,0 +1,36 @@
+import type { ResponseItem } from "openai/resources/responses/responses.mjs";
+
+/**
+ * Extracts the patch texts of all `apply_patch` tool calls from the given
+ * message history. Returns an empty string when none are found.
+ */
+export function extractAppliedPatches(items: Array): string {
+ const patches: Array = [];
+
+ for (const item of items) {
+ if (item.type !== "function_call") {
+ continue;
+ }
+
+ const { name: toolName, arguments: argsString } = item as unknown as {
+ name: unknown;
+ arguments: unknown;
+ };
+
+ if (toolName !== "apply_patch" || typeof argsString !== "string") {
+ continue;
+ }
+
+ try {
+ const args = JSON.parse(argsString) as { patch?: string };
+ if (typeof args.patch === "string" && args.patch.length > 0) {
+ patches.push(args.patch.trim());
+ }
+ } catch {
+ // Ignore malformed JSON – we never want to crash the overlay.
+ continue;
+ }
+ }
+
+ return patches.join("\n\n");
+}
diff --git a/codex-cli/src/utils/file-system-suggestions.ts b/codex-cli/src/utils/file-system-suggestions.ts
new file mode 100644
index 0000000000..13350c9a76
--- /dev/null
+++ b/codex-cli/src/utils/file-system-suggestions.ts
@@ -0,0 +1,42 @@
+import fs from "fs";
+import os from "os";
+import path from "path";
+
+export function getFileSystemSuggestions(pathPrefix: string): Array {
+ if (!pathPrefix) {
+ return [];
+ }
+
+ try {
+ const sep = path.sep;
+ const hasTilde = pathPrefix === "~" || pathPrefix.startsWith("~" + sep);
+ const expanded = hasTilde
+ ? path.join(os.homedir(), pathPrefix.slice(1))
+ : pathPrefix;
+
+ const normalized = path.normalize(expanded);
+ const isDir = pathPrefix.endsWith(path.sep);
+ const base = path.basename(normalized);
+
+ const dir =
+ normalized === "." && !pathPrefix.startsWith("." + sep) && !hasTilde
+ ? process.cwd()
+ : path.dirname(normalized);
+
+ const readDir = isDir ? path.join(dir, base) : dir;
+
+ return fs
+ .readdirSync(readDir)
+ .filter((item) => isDir || item.startsWith(base))
+ .map((item) => {
+ const fullPath = path.join(readDir, item);
+ const isDirectory = fs.statSync(fullPath).isDirectory();
+ if (isDirectory) {
+ return path.join(fullPath, sep);
+ }
+ return fullPath;
+ });
+ } catch {
+ return [];
+ }
+}
diff --git a/codex-cli/src/utils/get-diff.ts b/codex-cli/src/utils/get-diff.ts
new file mode 100644
index 0000000000..348ee42c78
--- /dev/null
+++ b/codex-cli/src/utils/get-diff.ts
@@ -0,0 +1,29 @@
+import { execSync } from "node:child_process";
+
+/**
+ * Returns the current Git diff for the working directory. If the current
+ * working directory is not inside a Git repository, `isGitRepo` will be
+ * false and `diff` will be an empty string.
+ */
+export function getGitDiff(): {
+ isGitRepo: boolean;
+ diff: string;
+} {
+ try {
+ // First check whether we are inside a git repository. `rev‑parse` exits
+ // with a non‑zero status code if not.
+ execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
+
+ // If the above call didn’t throw, we are inside a git repo. Retrieve the
+ // diff including color codes so that the overlay can render them.
+ const output = execSync("git diff --color", {
+ encoding: "utf8",
+ maxBuffer: 10 * 1024 * 1024, // 10 MB ought to be enough for now
+ });
+
+ return { isGitRepo: true, diff: output };
+ } catch {
+ // Either git is not installed or we’re not inside a repository.
+ return { isGitRepo: false, diff: "" };
+ }
+}
diff --git a/codex-cli/src/utils/input-utils.ts b/codex-cli/src/utils/input-utils.ts
index 80a238ba59..449f27cb43 100644
--- a/codex-cli/src/utils/input-utils.ts
+++ b/codex-cli/src/utils/input-utils.ts
@@ -2,6 +2,7 @@ import type { ResponseInputItem } from "openai/resources/responses/responses";
import { fileTypeFromBuffer } from "file-type";
import fs from "fs/promises";
+import path from "path";
export async function createInputItem(
text: string,
@@ -14,17 +15,24 @@ export async function createInputItem(
};
for (const filePath of images) {
- /* eslint-disable no-await-in-loop */
- const binary = await fs.readFile(filePath);
- const kind = await fileTypeFromBuffer(binary);
- /* eslint-enable no-await-in-loop */
- const encoded = binary.toString("base64");
- const mime = kind?.mime ?? "application/octet-stream";
- inputItem.content.push({
- type: "input_image",
- detail: "auto",
- image_url: `data:${mime};base64,${encoded}`,
- });
+ try {
+ /* eslint-disable no-await-in-loop */
+ const binary = await fs.readFile(filePath);
+ const kind = await fileTypeFromBuffer(binary);
+ /* eslint-enable no-await-in-loop */
+ const encoded = binary.toString("base64");
+ const mime = kind?.mime ?? "application/octet-stream";
+ inputItem.content.push({
+ type: "input_image",
+ detail: "auto",
+ image_url: `data:${mime};base64,${encoded}`,
+ });
+ } catch (err) {
+ inputItem.content.push({
+ type: "input_text",
+ text: `[missing image: ${path.basename(filePath)}]`,
+ });
+ }
}
return inputItem;
diff --git a/codex-cli/src/utils/agent/log.ts b/codex-cli/src/utils/logger/log.ts
similarity index 92%
rename from codex-cli/src/utils/agent/log.ts
rename to codex-cli/src/utils/logger/log.ts
index e804386566..e4aca06c79 100644
--- a/codex-cli/src/utils/agent/log.ts
+++ b/codex-cli/src/utils/logger/log.ts
@@ -124,6 +124,14 @@ export function log(message: string): void {
(logger ?? initLogger()).log(message);
}
+/**
+ * USE SPARINGLY! This function should only be used to guard a call to log() if
+ * the log message is large and you want to avoid constructing it if logging is
+ * disabled.
+ *
+ * `log()` is already a no-op if DEBUG is not set, so an extra
+ * `isLoggingEnabled()` check is unnecessary.
+ */
export function isLoggingEnabled(): boolean {
return (logger ?? initLogger()).isLoggingEnabled();
}
diff --git a/codex-cli/src/utils/model-info.ts b/codex-cli/src/utils/model-info.ts
new file mode 100644
index 0000000000..bbe0cb36a9
--- /dev/null
+++ b/codex-cli/src/utils/model-info.ts
@@ -0,0 +1,198 @@
+export type ModelInfo = {
+ /** The human-readable label for this model */
+ label: string;
+ /** The max context window size for this model */
+ maxContextLength: number;
+};
+
+export type SupportedModelId = keyof typeof openAiModelInfo;
+export const openAiModelInfo = {
+ "o1-pro-2025-03-19": {
+ label: "o1 Pro (2025-03-19)",
+ maxContextLength: 200000,
+ },
+ "o3": {
+ label: "o3",
+ maxContextLength: 200000,
+ },
+ "o3-2025-04-16": {
+ label: "o3 (2025-04-16)",
+ maxContextLength: 200000,
+ },
+ "o4-mini": {
+ label: "o4 Mini",
+ maxContextLength: 200000,
+ },
+ "gpt-4.1-nano": {
+ label: "GPT-4.1 Nano",
+ maxContextLength: 1000000,
+ },
+ "gpt-4.1-nano-2025-04-14": {
+ label: "GPT-4.1 Nano (2025-04-14)",
+ maxContextLength: 1000000,
+ },
+ "o4-mini-2025-04-16": {
+ label: "o4 Mini (2025-04-16)",
+ maxContextLength: 200000,
+ },
+ "gpt-4": {
+ label: "GPT-4",
+ maxContextLength: 8192,
+ },
+ "o1-preview-2024-09-12": {
+ label: "o1 Preview (2024-09-12)",
+ maxContextLength: 128000,
+ },
+ "gpt-4.1-mini": {
+ label: "GPT-4.1 Mini",
+ maxContextLength: 1000000,
+ },
+ "gpt-3.5-turbo-instruct-0914": {
+ label: "GPT-3.5 Turbo Instruct (0914)",
+ maxContextLength: 4096,
+ },
+ "gpt-4o-mini-search-preview": {
+ label: "GPT-4o Mini Search Preview",
+ maxContextLength: 128000,
+ },
+ "gpt-4.1-mini-2025-04-14": {
+ label: "GPT-4.1 Mini (2025-04-14)",
+ maxContextLength: 1000000,
+ },
+ "chatgpt-4o-latest": {
+ label: "ChatGPT-4o Latest",
+ maxContextLength: 128000,
+ },
+ "gpt-3.5-turbo-1106": {
+ label: "GPT-3.5 Turbo (1106)",
+ maxContextLength: 16385,
+ },
+ "gpt-4o-search-preview": {
+ label: "GPT-4o Search Preview",
+ maxContextLength: 128000,
+ },
+ "gpt-4-turbo": {
+ label: "GPT-4 Turbo",
+ maxContextLength: 128000,
+ },
+ "gpt-4o-realtime-preview-2024-12-17": {
+ label: "GPT-4o Realtime Preview (2024-12-17)",
+ maxContextLength: 128000,
+ },
+ "gpt-3.5-turbo-instruct": {
+ label: "GPT-3.5 Turbo Instruct",
+ maxContextLength: 4096,
+ },
+ "gpt-3.5-turbo": {
+ label: "GPT-3.5 Turbo",
+ maxContextLength: 16385,
+ },
+ "gpt-4-turbo-preview": {
+ label: "GPT-4 Turbo Preview",
+ maxContextLength: 128000,
+ },
+ "gpt-4o-mini-search-preview-2025-03-11": {
+ label: "GPT-4o Mini Search Preview (2025-03-11)",
+ maxContextLength: 128000,
+ },
+ "gpt-4-0125-preview": {
+ label: "GPT-4 (0125) Preview",
+ maxContextLength: 128000,
+ },
+ "gpt-4o-2024-11-20": {
+ label: "GPT-4o (2024-11-20)",
+ maxContextLength: 128000,
+ },
+ "o3-mini": {
+ label: "o3 Mini",
+ maxContextLength: 200000,
+ },
+ "gpt-4o-2024-05-13": {
+ label: "GPT-4o (2024-05-13)",
+ maxContextLength: 128000,
+ },
+ "gpt-4-turbo-2024-04-09": {
+ label: "GPT-4 Turbo (2024-04-09)",
+ maxContextLength: 128000,
+ },
+ "gpt-3.5-turbo-16k": {
+ label: "GPT-3.5 Turbo 16k",
+ maxContextLength: 16385,
+ },
+ "o3-mini-2025-01-31": {
+ label: "o3 Mini (2025-01-31)",
+ maxContextLength: 200000,
+ },
+ "o1-preview": {
+ label: "o1 Preview",
+ maxContextLength: 128000,
+ },
+ "o1-2024-12-17": {
+ label: "o1 (2024-12-17)",
+ maxContextLength: 128000,
+ },
+ "gpt-4-0613": {
+ label: "GPT-4 (0613)",
+ maxContextLength: 8192,
+ },
+ "o1": {
+ label: "o1",
+ maxContextLength: 128000,
+ },
+ "o1-pro": {
+ label: "o1 Pro",
+ maxContextLength: 200000,
+ },
+ "gpt-4.5-preview": {
+ label: "GPT-4.5 Preview",
+ maxContextLength: 128000,
+ },
+ "gpt-4.5-preview-2025-02-27": {
+ label: "GPT-4.5 Preview (2025-02-27)",
+ maxContextLength: 128000,
+ },
+ "gpt-4o-search-preview-2025-03-11": {
+ label: "GPT-4o Search Preview (2025-03-11)",
+ maxContextLength: 128000,
+ },
+ "gpt-4o": {
+ label: "GPT-4o",
+ maxContextLength: 128000,
+ },
+ "gpt-4o-mini": {
+ label: "GPT-4o Mini",
+ maxContextLength: 128000,
+ },
+ "gpt-4o-2024-08-06": {
+ label: "GPT-4o (2024-08-06)",
+ maxContextLength: 128000,
+ },
+ "gpt-4.1": {
+ label: "GPT-4.1",
+ maxContextLength: 1000000,
+ },
+ "gpt-4.1-2025-04-14": {
+ label: "GPT-4.1 (2025-04-14)",
+ maxContextLength: 1000000,
+ },
+ "gpt-4o-mini-2024-07-18": {
+ label: "GPT-4o Mini (2024-07-18)",
+ maxContextLength: 128000,
+ },
+ "o1-mini": {
+ label: "o1 Mini",
+ maxContextLength: 128000,
+ },
+ "gpt-3.5-turbo-0125": {
+ label: "GPT-3.5 Turbo (0125)",
+ maxContextLength: 16385,
+ },
+ "o1-mini-2024-09-12": {
+ label: "o1 Mini (2024-09-12)",
+ maxContextLength: 128000,
+ },
+ "gpt-4-1106-preview": {
+ label: "GPT-4 (1106) Preview",
+ maxContextLength: 128000,
+ },
+} as const satisfies Record;
diff --git a/codex-cli/src/utils/model-utils.ts b/codex-cli/src/utils/model-utils.ts
index 07d924c05b..5670fc44db 100644
--- a/codex-cli/src/utils/model-utils.ts
+++ b/codex-cli/src/utils/model-utils.ts
@@ -1,4 +1,8 @@
-import { OPENAI_API_KEY } from "./config";
+import type { ResponseItem } from "openai/resources/responses/responses.mjs";
+
+import { approximateTokensUsed } from "./approximate-tokens-used.js";
+import { getBaseUrl, getApiKey } from "./config";
+import { type SupportedModelId, openAiModelInfo } from "./model-info.js";
import OpenAI from "openai";
const MODEL_LIST_TIMEOUT_MS = 2_000; // 2 seconds
@@ -11,53 +15,49 @@ export const RECOMMENDED_MODELS: Array = ["o4-mini", "o3"];
* enters interactive mode. The request is made exactly once during the
* lifetime of the process and the results are cached for subsequent calls.
*/
-
-let modelsPromise: Promise> | null = null;
-
-async function fetchModels(): Promise> {
- // If the user has not configured an API key we cannot hit the network.
- if (!OPENAI_API_KEY) {
- return RECOMMENDED_MODELS;
+async function fetchModels(provider: string): Promise> {
+ // If the user has not configured an API key we cannot retrieve the models.
+ if (!getApiKey(provider)) {
+ throw new Error("No API key configured for provider: " + provider);
}
try {
- const openai = new OpenAI({ apiKey: OPENAI_API_KEY });
+ const openai = new OpenAI({
+ apiKey: getApiKey(provider),
+ baseURL: getBaseUrl(provider),
+ });
const list = await openai.models.list();
-
const models: Array = [];
for await (const model of list as AsyncIterable<{ id?: string }>) {
if (model && typeof model.id === "string") {
- models.push(model.id);
+ let modelStr = model.id;
+ // Fix for gemini.
+ if (modelStr.startsWith("models/")) {
+ modelStr = modelStr.replace("models/", "");
+ }
+ models.push(modelStr);
}
}
return models.sort();
- } catch {
+ } catch (error) {
return [];
}
}
-export function preloadModels(): void {
- if (!modelsPromise) {
- // Fire‑and‑forget – callers that truly need the list should `await`
- // `getAvailableModels()` instead.
- void getAvailableModels();
- }
-}
-
-export async function getAvailableModels(): Promise> {
- if (!modelsPromise) {
- modelsPromise = fetchModels();
- }
- return modelsPromise;
+/** Returns the list of models available for the provided key / credentials. */
+export async function getAvailableModels(
+ provider: string,
+): Promise> {
+ return fetchModels(provider.toLowerCase());
}
/**
- * Verify that the provided model identifier is present in the set returned by
- * {@link getAvailableModels}. The list of models is fetched from the OpenAI
- * `/models` endpoint the first time it is required and then cached in‑process.
+ * Verifies that the provided model identifier is present in the set returned by
+ * {@link getAvailableModels}.
*/
export async function isModelSupportedForResponses(
+ provider: string,
model: string | undefined | null,
): Promise {
if (
@@ -70,7 +70,7 @@ export async function isModelSupportedForResponses(
try {
const models = await Promise.race>([
- getAvailableModels(),
+ getAvailableModels(provider),
new Promise>((resolve) =>
setTimeout(() => resolve([]), MODEL_LIST_TIMEOUT_MS),
),
@@ -88,3 +88,112 @@ export async function isModelSupportedForResponses(
return true;
}
}
+
+/** Returns the maximum context length (in tokens) for a given model. */
+export function maxTokensForModel(model: string): number {
+ if (model in openAiModelInfo) {
+ return openAiModelInfo[model as SupportedModelId].maxContextLength;
+ }
+
+ // fallback to heuristics for models not in the registry
+ const lower = model.toLowerCase();
+ if (lower.includes("32k")) {
+ return 32000;
+ }
+ if (lower.includes("16k")) {
+ return 16000;
+ }
+ if (lower.includes("8k")) {
+ return 8000;
+ }
+ if (lower.includes("4k")) {
+ return 4000;
+ }
+ return 128000; // Default to 128k for any other model.
+}
+
+/** Calculates the percentage of tokens remaining in context for a model. */
+export function calculateContextPercentRemaining(
+ items: Array,
+ model: string,
+): number {
+ const used = approximateTokensUsed(items);
+ const max = maxTokensForModel(model);
+ const remaining = Math.max(0, max - used);
+ return (remaining / max) * 100;
+}
+
+/**
+ * Type‑guard that narrows a {@link ResponseItem} to one that represents a
+ * user‑authored message. The OpenAI SDK represents both input *and* output
+ * messages with a discriminated union where:
+ * • `type` is the string literal "message" and
+ * • `role` is one of "user" | "assistant" | "system" | "developer".
+ *
+ * For the purposes of de‑duplication we only care about *user* messages so we
+ * detect those here in a single, reusable helper.
+ */
+function isUserMessage(
+ item: ResponseItem,
+): item is ResponseItem & { type: "message"; role: "user"; content: unknown } {
+ return item.type === "message" && (item as { role?: string }).role === "user";
+}
+
+/**
+ * Deduplicate the stream of {@link ResponseItem}s before they are persisted in
+ * component state.
+ *
+ * Historically we used the (optional) {@code id} field returned by the
+ * OpenAI streaming API as the primary key: the first occurrence of any given
+ * {@code id} “won” and subsequent duplicates were dropped. In practice this
+ * proved brittle because locally‑generated user messages don’t include an
+ * {@code id}. The result was that if a user quickly pressed twice the
+ * exact same message would appear twice in the transcript.
+ *
+ * The new rules are therefore:
+ * 1. If a {@link ResponseItem} has an {@code id} keep only the *first*
+ * occurrence of that {@code id} (this retains the previous behaviour for
+ * assistant / tool messages).
+ * 2. Additionally, collapse *consecutive* user messages with identical
+ * content. Two messages are considered identical when their serialized
+ * {@code content} array matches exactly. We purposefully restrict this
+ * to **adjacent** duplicates so that legitimately repeated questions at
+ * a later point in the conversation are still shown.
+ */
+export function uniqueById(items: Array): Array {
+ const seenIds = new Set();
+ const deduped: Array = [];
+
+ for (const item of items) {
+ // ──────────────────────────────────────────────────────────────────
+ // Rule #1 – de‑duplicate by id when present
+ // ──────────────────────────────────────────────────────────────────
+ if (typeof item.id === "string" && item.id.length > 0) {
+ if (seenIds.has(item.id)) {
+ continue; // skip duplicates
+ }
+ seenIds.add(item.id);
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // Rule #2 – collapse consecutive identical user messages
+ // ──────────────────────────────────────────────────────────────────
+ if (isUserMessage(item) && deduped.length > 0) {
+ const prev = deduped[deduped.length - 1]!;
+
+ if (
+ isUserMessage(prev) &&
+ // Note: the `content` field is an array of message parts. Performing
+ // a deep compare is over‑kill here; serialising to JSON is sufficient
+ // (and fast for the tiny payloads involved).
+ JSON.stringify(prev.content) === JSON.stringify(item.content)
+ ) {
+ continue; // skip duplicate user message
+ }
+ }
+
+ deduped.push(item);
+ }
+
+ return deduped;
+}
diff --git a/codex-cli/src/utils/package-manager-detector.ts b/codex-cli/src/utils/package-manager-detector.ts
new file mode 100644
index 0000000000..ecd8d9c7f6
--- /dev/null
+++ b/codex-cli/src/utils/package-manager-detector.ts
@@ -0,0 +1,73 @@
+import type { AgentName } from "package-manager-detector";
+
+import { execFileSync } from "node:child_process";
+import { join, resolve } from "node:path";
+import which from "which";
+
+function isInstalled(manager: AgentName): boolean {
+ try {
+ which.sync(manager);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+function getGlobalBinDir(manager: AgentName): string | undefined {
+ if (!isInstalled(manager)) {
+ return;
+ }
+
+ try {
+ switch (manager) {
+ case "npm": {
+ const stdout = execFileSync("npm", ["prefix", "-g"], {
+ encoding: "utf-8",
+ });
+ return join(stdout.trim(), "bin");
+ }
+
+ case "pnpm": {
+ // pnpm bin -g prints the bin dir
+ const stdout = execFileSync("pnpm", ["bin", "-g"], {
+ encoding: "utf-8",
+ });
+ return stdout.trim();
+ }
+
+ case "bun": {
+ // bun pm bin -g prints your bun global bin folder
+ const stdout = execFileSync("bun", ["pm", "bin", "-g"], {
+ encoding: "utf-8",
+ });
+ return stdout.trim();
+ }
+
+ default:
+ return undefined;
+ }
+ } catch {
+ // ignore
+ }
+
+ return undefined;
+}
+
+export async function detectInstallerByPath(): Promise {
+ // e.g. /usr/local/bin/codex
+ const invoked = process.argv[1] && resolve(process.argv[1]);
+ if (!invoked) {
+ return;
+ }
+
+ const supportedManagers: Array = ["npm", "pnpm", "bun"];
+
+ for (const mgr of supportedManagers) {
+ const binDir = getGlobalBinDir(mgr);
+ if (binDir && invoked.startsWith(binDir)) {
+ return mgr;
+ }
+ }
+
+ return undefined;
+}
diff --git a/codex-cli/src/utils/parsers.ts b/codex-cli/src/utils/parsers.ts
index 4461379c02..ed86833f51 100644
--- a/codex-cli/src/utils/parsers.ts
+++ b/codex-cli/src/utils/parsers.ts
@@ -81,7 +81,13 @@ export function parseToolCallArguments(
}
const { cmd, command } = json as Record;
- const commandArray = toStringArray(cmd) ?? toStringArray(command);
+ // The OpenAI model sometimes produces a single string instead of an array.
+ // Accept both shapes:
+ const commandArray =
+ toStringArray(cmd) ??
+ toStringArray(command) ??
+ (typeof cmd === "string" ? [cmd] : undefined) ??
+ (typeof command === "string" ? [command] : undefined);
if (commandArray == null) {
return undefined;
}
diff --git a/codex-cli/src/utils/providers.ts b/codex-cli/src/utils/providers.ts
new file mode 100644
index 0000000000..adf628bba2
--- /dev/null
+++ b/codex-cli/src/utils/providers.ts
@@ -0,0 +1,45 @@
+export const providers: Record<
+ string,
+ { name: string; baseURL: string; envKey: string }
+> = {
+ openai: {
+ name: "OpenAI",
+ baseURL: "https://api.openai.com/v1",
+ envKey: "OPENAI_API_KEY",
+ },
+ openrouter: {
+ name: "OpenRouter",
+ baseURL: "https://openrouter.ai/api/v1",
+ envKey: "OPENROUTER_API_KEY",
+ },
+ gemini: {
+ name: "Gemini",
+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
+ envKey: "GEMINI_API_KEY",
+ },
+ ollama: {
+ name: "Ollama",
+ baseURL: "http://localhost:11434/v1",
+ envKey: "OLLAMA_API_KEY",
+ },
+ mistral: {
+ name: "Mistral",
+ baseURL: "https://api.mistral.ai/v1",
+ envKey: "MISTRAL_API_KEY",
+ },
+ deepseek: {
+ name: "DeepSeek",
+ baseURL: "https://api.deepseek.com",
+ envKey: "DEEPSEEK_API_KEY",
+ },
+ xai: {
+ name: "xAI",
+ baseURL: "https://api.x.ai/v1",
+ envKey: "XAI_API_KEY",
+ },
+ groq: {
+ name: "Groq",
+ baseURL: "https://api.groq.com/openai/v1",
+ envKey: "GROQ_API_KEY",
+ },
+};
diff --git a/codex-cli/src/utils/responses.ts b/codex-cli/src/utils/responses.ts
new file mode 100644
index 0000000000..6a763ebfa0
--- /dev/null
+++ b/codex-cli/src/utils/responses.ts
@@ -0,0 +1,717 @@
+import type { OpenAI } from "openai";
+import type {
+ ResponseCreateParams,
+ Response,
+} from "openai/resources/responses/responses";
+
+// Define interfaces based on OpenAI API documentation
+type ResponseCreateInput = ResponseCreateParams;
+type ResponseOutput = Response;
+// interface ResponseOutput {
+// id: string;
+// object: 'response';
+// created_at: number;
+// status: 'completed' | 'failed' | 'in_progress' | 'incomplete';
+// error: { code: string; message: string } | null;
+// incomplete_details: { reason: string } | null;
+// instructions: string | null;
+// max_output_tokens: number | null;
+// model: string;
+// output: Array<{
+// type: 'message';
+// id: string;
+// status: 'completed' | 'in_progress';
+// role: 'assistant';
+// content: Array<{
+// type: 'output_text' | 'function_call';
+// text?: string;
+// annotations?: Array;
+// tool_call?: {
+// id: string;
+// type: 'function';
+// function: { name: string; arguments: string };
+// };
+// }>;
+// }>;
+// parallel_tool_calls: boolean;
+// previous_response_id: string | null;
+// reasoning: { effort: string | null; summary: string | null };
+// store: boolean;
+// temperature: number;
+// text: { format: { type: 'text' } };
+// tool_choice: string | object;
+// tools: Array;
+// top_p: number;
+// truncation: string;
+// usage: {
+// input_tokens: number;
+// input_tokens_details: { cached_tokens: number };
+// output_tokens: number;
+// output_tokens_details: { reasoning_tokens: number };
+// total_tokens: number;
+// } | null;
+// user: string | null;
+// metadata: Record;
+// }
+
+// Define types for the ResponseItem content and parts
+type ResponseContentPart = {
+ type: string;
+ [key: string]: unknown;
+};
+
+type ResponseItemType = {
+ type: string;
+ id?: string;
+ status?: string;
+ role?: string;
+ content?: Array;
+ [key: string]: unknown;
+};
+
+type ResponseEvent =
+ | { type: "response.created"; response: Partial }
+ | { type: "response.in_progress"; response: Partial }
+ | {
+ type: "response.output_item.added";
+ output_index: number;
+ item: ResponseItemType;
+ }
+ | {
+ type: "response.content_part.added";
+ item_id: string;
+ output_index: number;
+ content_index: number;
+ part: ResponseContentPart;
+ }
+ | {
+ type: "response.output_text.delta";
+ item_id: string;
+ output_index: number;
+ content_index: number;
+ delta: string;
+ }
+ | {
+ type: "response.output_text.done";
+ item_id: string;
+ output_index: number;
+ content_index: number;
+ text: string;
+ }
+ | {
+ type: "response.function_call_arguments.delta";
+ item_id: string;
+ output_index: number;
+ content_index: number;
+ delta: string;
+ }
+ | {
+ type: "response.function_call_arguments.done";
+ item_id: string;
+ output_index: number;
+ content_index: number;
+ arguments: string;
+ }
+ | {
+ type: "response.content_part.done";
+ item_id: string;
+ output_index: number;
+ content_index: number;
+ part: ResponseContentPart;
+ }
+ | {
+ type: "response.output_item.done";
+ output_index: number;
+ item: ResponseItemType;
+ }
+ | { type: "response.completed"; response: ResponseOutput }
+ | { type: "error"; code: string; message: string; param: string | null };
+
+// Define a type for tool call data
+type ToolCallData = {
+ id: string;
+ name: string;
+ arguments: string;
+};
+
+// Define a type for usage data
+type UsageData = {
+ prompt_tokens?: number;
+ completion_tokens?: number;
+ total_tokens?: number;
+ input_tokens?: number;
+ input_tokens_details?: { cached_tokens: number };
+ output_tokens?: number;
+ output_tokens_details?: { reasoning_tokens: number };
+ [key: string]: unknown;
+};
+
+// Define a type for content output
+type ResponseContentOutput =
+ | {
+ type: "function_call";
+ call_id: string;
+ name: string;
+ arguments: string;
+ [key: string]: unknown;
+ }
+ | {
+ type: "output_text";
+ text: string;
+ annotations: Array;
+ [key: string]: unknown;
+ };
+
+// Global map to store conversation histories
+const conversationHistories = new Map<
+ string,
+ {
+ previous_response_id: string | null;
+ messages: Array;
+ }
+>();
+
+// Utility function to generate unique IDs
+function generateId(prefix: string = "msg"): string {
+ return `${prefix}_${Math.random().toString(36).substr(2, 9)}`;
+}
+
+// Function to convert ResponseInputItem to ChatCompletionMessageParam
+type ResponseInputItem = ResponseCreateInput["input"][number];
+
+function convertInputItemToMessage(
+ item: string | ResponseInputItem,
+): OpenAI.Chat.Completions.ChatCompletionMessageParam {
+ // Handle string inputs as content for a user message
+ if (typeof item === "string") {
+ return { role: "user", content: item };
+ }
+
+ // At this point we know it's a ResponseInputItem
+ const responseItem = item;
+
+ if (responseItem.type === "message") {
+ // Use a more specific type assertion for the message content
+ const content = Array.isArray(responseItem.content)
+ ? responseItem.content
+ .filter((c) => typeof c === "object" && c.type === "input_text")
+ .map((c) =>
+ typeof c === "object" && "text" in c
+ ? (c["text"] as string) || ""
+ : "",
+ )
+ .join("")
+ : "";
+ return { role: responseItem.role, content };
+ } else if (responseItem.type === "function_call_output") {
+ return {
+ role: "tool",
+ tool_call_id: responseItem.call_id,
+ content: responseItem.output,
+ };
+ }
+ throw new Error(`Unsupported input item type: ${responseItem.type}`);
+}
+
+// Function to get full messages including history
+function getFullMessages(
+ input: ResponseCreateInput,
+): Array {
+ let baseHistory: Array =
+ [];
+ if (input.previous_response_id) {
+ const prev = conversationHistories.get(input.previous_response_id);
+ if (!prev) {
+ throw new Error(
+ `Previous response not found: ${input.previous_response_id}`,
+ );
+ }
+ baseHistory = prev.messages;
+ }
+
+ // Handle both string and ResponseInputItem in input.input
+ const newInputMessages = Array.isArray(input.input)
+ ? input.input.map(convertInputItemToMessage)
+ : [convertInputItemToMessage(input.input)];
+
+ const messages = [...baseHistory, ...newInputMessages];
+ if (
+ input.instructions &&
+ messages[0]?.role !== "system" &&
+ messages[0]?.role !== "developer"
+ ) {
+ return [{ role: "system", content: input.instructions }, ...messages];
+ }
+ return messages;
+}
+
+// Function to convert tools
+function convertTools(
+ tools?: ResponseCreateInput["tools"],
+): Array | undefined {
+ return tools
+ ?.filter((tool) => tool.type === "function")
+ .map((tool) => ({
+ type: "function" as const,
+ function: {
+ name: tool.name,
+ description: tool.description || undefined,
+ parameters: tool.parameters,
+ },
+ }));
+}
+
+const createCompletion = (openai: OpenAI, input: ResponseCreateInput) => {
+ const fullMessages = getFullMessages(input);
+ const chatTools = convertTools(input.tools);
+ const webSearchOptions = input.tools?.some(
+ (tool) => tool.type === "function" && tool.name === "web_search",
+ )
+ ? {}
+ : undefined;
+
+ const chatInput: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
+ model: input.model,
+ messages: fullMessages,
+ tools: chatTools,
+ web_search_options: webSearchOptions,
+ temperature: input.temperature,
+ top_p: input.top_p,
+ tool_choice: (input.tool_choice === "auto"
+ ? "auto"
+ : input.tool_choice) as OpenAI.Chat.Completions.ChatCompletionCreateParams["tool_choice"],
+ stream: input.stream || false,
+ user: input.user,
+ metadata: input.metadata,
+ };
+
+ return openai.chat.completions.create(chatInput);
+};
+
+// Main function with overloading
+async function responsesCreateViaChatCompletions(
+ openai: OpenAI,
+ input: ResponseCreateInput & { stream: true },
+): Promise>;
+async function responsesCreateViaChatCompletions(
+ openai: OpenAI,
+ input: ResponseCreateInput & { stream?: false },
+): Promise;
+async function responsesCreateViaChatCompletions(
+ openai: OpenAI,
+ input: ResponseCreateInput,
+): Promise> {
+ const completion = await createCompletion(openai, input);
+ if (input.stream) {
+ return streamResponses(
+ input,
+ completion as AsyncIterable,
+ );
+ } else {
+ return nonStreamResponses(
+ input,
+ completion as unknown as OpenAI.Chat.Completions.ChatCompletion,
+ );
+ }
+}
+
+// Non-streaming implementation
+async function nonStreamResponses(
+ input: ResponseCreateInput,
+ completion: OpenAI.Chat.Completions.ChatCompletion,
+): Promise {
+ const fullMessages = getFullMessages(input);
+
+ try {
+ const chatResponse = completion;
+ if (!("choices" in chatResponse) || chatResponse.choices.length === 0) {
+ throw new Error("No choices in chat completion response");
+ }
+ const assistantMessage = chatResponse.choices?.[0]?.message;
+ if (!assistantMessage) {
+ throw new Error("No assistant message in chat completion response");
+ }
+
+ // Construct ResponseOutput
+ const responseId = generateId("resp");
+ const outputItemId = generateId("msg");
+ const outputContent: Array = [];
+
+ // Check if the response contains tool calls
+ const hasFunctionCalls =
+ assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0;
+
+ if (hasFunctionCalls && assistantMessage.tool_calls) {
+ for (const toolCall of assistantMessage.tool_calls) {
+ if (toolCall.type === "function") {
+ outputContent.push({
+ type: "function_call",
+ call_id: toolCall.id,
+ name: toolCall.function.name,
+ arguments: toolCall.function.arguments,
+ });
+ }
+ }
+ }
+
+ if (assistantMessage.content) {
+ outputContent.push({
+ type: "output_text",
+ text: assistantMessage.content,
+ annotations: [],
+ });
+ }
+
+ // Create response with appropriate status and properties
+ const responseOutput = {
+ id: responseId,
+ object: "response",
+ created_at: Math.floor(Date.now() / 1000),
+ status: hasFunctionCalls ? "requires_action" : "completed",
+ error: null,
+ incomplete_details: null,
+ instructions: null,
+ max_output_tokens: null,
+ model: chatResponse.model,
+ output: [
+ {
+ type: "message",
+ id: outputItemId,
+ status: "completed",
+ role: "assistant",
+ content: outputContent,
+ },
+ ],
+ parallel_tool_calls: input.parallel_tool_calls ?? false,
+ previous_response_id: input.previous_response_id ?? null,
+ reasoning: null,
+ temperature: input.temperature,
+ text: { format: { type: "text" } },
+ tool_choice: input.tool_choice ?? "auto",
+ tools: input.tools ?? [],
+ top_p: input.top_p,
+ truncation: input.truncation ?? "disabled",
+ usage: chatResponse.usage
+ ? {
+ input_tokens: chatResponse.usage.prompt_tokens,
+ input_tokens_details: { cached_tokens: 0 },
+ output_tokens: chatResponse.usage.completion_tokens,
+ output_tokens_details: { reasoning_tokens: 0 },
+ total_tokens: chatResponse.usage.total_tokens,
+ }
+ : undefined,
+ user: input.user ?? undefined,
+ metadata: input.metadata ?? {},
+ output_text: "",
+ } as ResponseOutput;
+
+ // Add required_action property for tool calls
+ if (hasFunctionCalls && assistantMessage.tool_calls) {
+ // Define type with required action
+ type ResponseWithAction = Partial & {
+ required_action: unknown;
+ };
+
+ // Use the defined type for the assertion
+ (responseOutput as ResponseWithAction).required_action = {
+ type: "submit_tool_outputs",
+ submit_tool_outputs: {
+ tool_calls: assistantMessage.tool_calls.map((toolCall) => ({
+ id: toolCall.id,
+ type: toolCall.type,
+ function: {
+ name: toolCall.function.name,
+ arguments: toolCall.function.arguments,
+ },
+ })),
+ },
+ };
+ }
+
+ // Store history
+ const newHistory = [...fullMessages, assistantMessage];
+ conversationHistories.set(responseId, {
+ previous_response_id: input.previous_response_id ?? null,
+ messages: newHistory,
+ });
+
+ return responseOutput;
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ throw new Error(`Failed to process chat completion: ${errorMessage}`);
+ }
+}
+
+// Streaming implementation
+async function* streamResponses(
+ input: ResponseCreateInput,
+ completion: AsyncIterable,
+): AsyncGenerator {
+ const fullMessages = getFullMessages(input);
+
+ const responseId = generateId("resp");
+ const outputItemId = generateId("msg");
+ let textContentAdded = false;
+ let textContent = "";
+ const toolCalls = new Map();
+ let usage: UsageData | null = null;
+ const finalOutputItem: Array = [];
+ // Initial response
+ const initialResponse: Partial = {
+ id: responseId,
+ object: "response" as const,
+ created_at: Math.floor(Date.now() / 1000),
+ status: "in_progress" as const,
+ model: input.model,
+ output: [],
+ error: null,
+ incomplete_details: null,
+ instructions: null,
+ max_output_tokens: null,
+ parallel_tool_calls: true,
+ previous_response_id: input.previous_response_id ?? null,
+ reasoning: null,
+ temperature: input.temperature,
+ text: { format: { type: "text" } },
+ tool_choice: input.tool_choice ?? "auto",
+ tools: input.tools ?? [],
+ top_p: input.top_p,
+ truncation: input.truncation ?? "disabled",
+ usage: undefined,
+ user: input.user ?? undefined,
+ metadata: input.metadata ?? {},
+ output_text: "",
+ };
+ yield { type: "response.created", response: initialResponse };
+ yield { type: "response.in_progress", response: initialResponse };
+ let isToolCall = false;
+ for await (const chunk of completion as AsyncIterable) {
+ // console.error('\nCHUNK: ', JSON.stringify(chunk));
+ const choice = chunk.choices[0];
+ if (!choice) {
+ continue;
+ }
+ if (
+ !isToolCall &&
+ (("tool_calls" in choice.delta && choice.delta.tool_calls) ||
+ choice.finish_reason === "tool_calls")
+ ) {
+ isToolCall = true;
+ }
+
+ if (chunk.usage) {
+ usage = {
+ prompt_tokens: chunk.usage.prompt_tokens,
+ completion_tokens: chunk.usage.completion_tokens,
+ total_tokens: chunk.usage.total_tokens,
+ input_tokens: chunk.usage.prompt_tokens,
+ input_tokens_details: { cached_tokens: 0 },
+ output_tokens: chunk.usage.completion_tokens,
+ output_tokens_details: { reasoning_tokens: 0 },
+ };
+ }
+ if (isToolCall) {
+ for (const tcDelta of choice.delta.tool_calls || []) {
+ const tcIndex = tcDelta.index;
+ const content_index = textContentAdded ? tcIndex + 1 : tcIndex;
+
+ if (!toolCalls.has(tcIndex)) {
+ // New tool call
+ const toolCallId = tcDelta.id || generateId("call");
+ const functionName = tcDelta.function?.name || "";
+
+ yield {
+ type: "response.output_item.added",
+ item: {
+ type: "function_call",
+ id: outputItemId,
+ status: "in_progress",
+ call_id: toolCallId,
+ name: functionName,
+ arguments: "",
+ },
+ output_index: 0,
+ };
+ toolCalls.set(tcIndex, {
+ id: toolCallId,
+ name: functionName,
+ arguments: "",
+ });
+ }
+
+ if (tcDelta.function?.arguments) {
+ const current = toolCalls.get(tcIndex);
+ if (current) {
+ current.arguments += tcDelta.function.arguments;
+ yield {
+ type: "response.function_call_arguments.delta",
+ item_id: outputItemId,
+ output_index: 0,
+ content_index,
+ delta: tcDelta.function.arguments,
+ };
+ }
+ }
+ }
+
+ if (choice.finish_reason === "tool_calls") {
+ for (const [tcIndex, tc] of toolCalls) {
+ const item = {
+ type: "function_call",
+ id: outputItemId,
+ status: "completed",
+ call_id: tc.id,
+ name: tc.name,
+ arguments: tc.arguments,
+ };
+ yield {
+ type: "response.function_call_arguments.done",
+ item_id: outputItemId,
+ output_index: tcIndex,
+ content_index: textContentAdded ? tcIndex + 1 : tcIndex,
+ arguments: tc.arguments,
+ };
+ yield {
+ type: "response.output_item.done",
+ output_index: tcIndex,
+ item,
+ };
+ finalOutputItem.push(item as unknown as ResponseContentOutput);
+ }
+ } else {
+ continue;
+ }
+ } else {
+ if (!textContentAdded) {
+ yield {
+ type: "response.content_part.added",
+ item_id: outputItemId,
+ output_index: 0,
+ content_index: 0,
+ part: { type: "output_text", text: "", annotations: [] },
+ };
+ textContentAdded = true;
+ }
+ if (choice.delta.content?.length) {
+ yield {
+ type: "response.output_text.delta",
+ item_id: outputItemId,
+ output_index: 0,
+ content_index: 0,
+ delta: choice.delta.content,
+ };
+ textContent += choice.delta.content;
+ }
+ if (choice.finish_reason) {
+ yield {
+ type: "response.output_text.done",
+ item_id: outputItemId,
+ output_index: 0,
+ content_index: 0,
+ text: textContent,
+ };
+ yield {
+ type: "response.content_part.done",
+ item_id: outputItemId,
+ output_index: 0,
+ content_index: 0,
+ part: { type: "output_text", text: textContent, annotations: [] },
+ };
+ const item = {
+ type: "message",
+ id: outputItemId,
+ status: "completed",
+ role: "assistant",
+ content: [
+ { type: "output_text", text: textContent, annotations: [] },
+ ],
+ };
+ yield {
+ type: "response.output_item.done",
+ output_index: 0,
+ item,
+ };
+ finalOutputItem.push(item as unknown as ResponseContentOutput);
+ } else {
+ continue;
+ }
+ }
+
+ // Construct final response
+ const finalResponse: ResponseOutput = {
+ id: responseId,
+ object: "response" as const,
+ created_at: initialResponse.created_at || Math.floor(Date.now() / 1000),
+ status: "completed" as const,
+ error: null,
+ incomplete_details: null,
+ instructions: null,
+ max_output_tokens: null,
+ model: chunk.model || input.model,
+ output: finalOutputItem as unknown as ResponseOutput["output"],
+ parallel_tool_calls: true,
+ previous_response_id: input.previous_response_id ?? null,
+ reasoning: null,
+ temperature: input.temperature,
+ text: { format: { type: "text" } },
+ tool_choice: input.tool_choice ?? "auto",
+ tools: input.tools ?? [],
+ top_p: input.top_p,
+ truncation: input.truncation ?? "disabled",
+ usage: usage as ResponseOutput["usage"],
+ user: input.user ?? undefined,
+ metadata: input.metadata ?? {},
+ output_text: "",
+ } as ResponseOutput;
+
+ // Store history
+ const assistantMessage: OpenAI.Chat.Completions.ChatCompletionMessageParam =
+ {
+ role: "assistant" as const,
+ };
+
+ if (textContent) {
+ assistantMessage.content = textContent;
+ }
+
+ // Add tool_calls property if needed
+ if (toolCalls.size > 0) {
+ const toolCallsArray = Array.from(toolCalls.values()).map((tc) => ({
+ id: tc.id,
+ type: "function" as const,
+ function: { name: tc.name, arguments: tc.arguments },
+ }));
+
+ // Define a more specific type for the assistant message with tool calls
+ type AssistantMessageWithToolCalls =
+ OpenAI.Chat.Completions.ChatCompletionMessageParam & {
+ tool_calls: Array<{
+ id: string;
+ type: "function";
+ function: {
+ name: string;
+ arguments: string;
+ };
+ }>;
+ };
+
+ // Use type assertion with the defined type
+ (assistantMessage as AssistantMessageWithToolCalls).tool_calls =
+ toolCallsArray;
+ }
+ const newHistory = [...fullMessages, assistantMessage];
+ conversationHistories.set(responseId, {
+ previous_response_id: input.previous_response_id ?? null,
+ messages: newHistory,
+ });
+
+ yield { type: "response.completed", response: finalResponse };
+ }
+}
+
+export {
+ responsesCreateViaChatCompletions,
+ ResponseCreateInput,
+ ResponseOutput,
+ ResponseEvent,
+};
diff --git a/codex-cli/src/utils/session.ts b/codex-cli/src/utils/session.ts
index 35d1873c32..c439f49094 100644
--- a/codex-cli/src/utils/session.ts
+++ b/codex-cli/src/utils/session.ts
@@ -1,4 +1,4 @@
-export const CLI_VERSION = "0.1.2504172351"; // Must be in sync with package.json.
+export const CLI_VERSION = "0.1.2504221401"; // Must be in sync with package.json.
export const ORIGIN = "codex_cli_ts";
export type TerminalChatSession = {
diff --git a/codex-cli/src/utils/slash-commands.ts b/codex-cli/src/utils/slash-commands.ts
new file mode 100644
index 0000000000..b276c49135
--- /dev/null
+++ b/codex-cli/src/utils/slash-commands.ts
@@ -0,0 +1,32 @@
+// Defines the available slash commands and their descriptions.
+// Used for autocompletion in the chat input.
+export interface SlashCommand {
+ command: string;
+ description: string;
+}
+
+export const SLASH_COMMANDS: Array = [
+ {
+ command: "/clear",
+ description: "Clear conversation history and free up context",
+ },
+ {
+ command: "/clearhistory",
+ description: "Clear command history",
+ },
+ {
+ command: "/compact",
+ description:
+ "Clear conversation history but keep a summary in context. Optional: /compact [instructions for summarization]",
+ },
+ { command: "/history", description: "Open command history" },
+ { command: "/help", description: "Show list of commands" },
+ { command: "/model", description: "Open model selection panel" },
+ { command: "/approval", description: "Open approval mode selection panel" },
+ { command: "/bug", description: "Generate a prefilled GitHub bug report" },
+ {
+ command: "/diff",
+ description:
+ "Show git diff of the working directory (or applied patches if not in git)",
+ },
+];
diff --git a/codex-cli/src/utils/storage/command-history.ts b/codex-cli/src/utils/storage/command-history.ts
index 997c4872ad..d774b53340 100644
--- a/codex-cli/src/utils/storage/command-history.ts
+++ b/codex-cli/src/utils/storage/command-history.ts
@@ -1,12 +1,13 @@
+import { log } from "../logger/log.js";
import { existsSync } from "fs";
import fs from "fs/promises";
import os from "os";
import path from "path";
const HISTORY_FILE = path.join(os.homedir(), ".codex", "history.json");
-const DEFAULT_HISTORY_SIZE = 1000;
+const DEFAULT_HISTORY_SIZE = 10_000;
-// Regex patterns for sensitive commands that should not be saved
+// Regex patterns for sensitive commands that should not be saved.
const SENSITIVE_PATTERNS = [
/\b[A-Za-z0-9-_]{20,}\b/, // API keys and tokens
/\bpassword\b/i,
@@ -18,7 +19,7 @@ const SENSITIVE_PATTERNS = [
export interface HistoryConfig {
maxSize: number;
saveHistory: boolean;
- sensitivePatterns: Array; // Array of regex patterns as strings
+ sensitivePatterns: Array; // Regex patterns.
}
export interface HistoryEntry {
@@ -32,9 +33,6 @@ export const DEFAULT_HISTORY_CONFIG: HistoryConfig = {
sensitivePatterns: [],
};
-/**
- * Loads command history from the history file
- */
export async function loadCommandHistory(): Promise> {
try {
if (!existsSync(HISTORY_FILE)) {
@@ -45,26 +43,21 @@ export async function loadCommandHistory(): Promise> {
const history = JSON.parse(data) as Array;
return Array.isArray(history) ? history : [];
} catch (error) {
- // Use error logger but for production would use a proper logging system
- // eslint-disable-next-line no-console
- console.error("Failed to load command history:", error);
+ log(`error: failed to load command history: ${error}`);
return [];
}
}
-/**
- * Saves command history to the history file
- */
export async function saveCommandHistory(
history: Array,
config: HistoryConfig = DEFAULT_HISTORY_CONFIG,
): Promise {
try {
- // Create directory if it doesn't exist
+ // Create directory if it doesn't exist.
const dir = path.dirname(HISTORY_FILE);
await fs.mkdir(dir, { recursive: true });
- // Trim history to max size
+ // Trim history to max size.
const trimmedHistory = history.slice(-config.maxSize);
await fs.writeFile(
@@ -73,14 +66,10 @@ export async function saveCommandHistory(
"utf-8",
);
} catch (error) {
- // eslint-disable-next-line no-console
- console.error("Failed to save command history:", error);
+ log(`error: failed to save command history: ${error}`);
}
}
-/**
- * Adds a command to history if it's not sensitive
- */
export async function addToHistory(
command: string,
history: Array,
@@ -90,46 +79,41 @@ export async function addToHistory(
return history;
}
- // Check if command contains sensitive information
- if (isSensitiveCommand(command, config.sensitivePatterns)) {
+ // Skip commands with sensitive information.
+ if (commandHasSensitiveInfo(command, config.sensitivePatterns)) {
return history;
}
- // Check for duplicate (don't add if it's the same as the last command)
+ // Check for duplicate (don't add if it's the same as the last command).
const lastEntry = history[history.length - 1];
if (lastEntry && lastEntry.command === command) {
return history;
}
- // Add new entry
- const newEntry: HistoryEntry = {
- command,
- timestamp: Date.now(),
- };
-
- const newHistory = [...history, newEntry];
-
- // Save to file
+ // Add new entry.
+ const newHistory: Array = [
+ ...history,
+ {
+ command,
+ timestamp: Date.now(),
+ },
+ ];
await saveCommandHistory(newHistory, config);
-
return newHistory;
}
-/**
- * Checks if a command contains sensitive information
- */
-function isSensitiveCommand(
+function commandHasSensitiveInfo(
command: string,
additionalPatterns: Array = [],
): boolean {
- // Check built-in patterns
+ // Check built-in patterns.
for (const pattern of SENSITIVE_PATTERNS) {
if (pattern.test(command)) {
return true;
}
}
- // Check additional patterns from config
+ // Check additional patterns from config.
for (const patternStr of additionalPatterns) {
try {
const pattern = new RegExp(patternStr);
@@ -137,23 +121,19 @@ function isSensitiveCommand(
return true;
}
} catch (error) {
- // Invalid regex pattern, skip it
+ // Invalid regex pattern, skip it.
}
}
return false;
}
-/**
- * Clears the command history
- */
export async function clearCommandHistory(): Promise {
try {
if (existsSync(HISTORY_FILE)) {
await fs.writeFile(HISTORY_FILE, JSON.stringify([]), "utf-8");
}
} catch (error) {
- // eslint-disable-next-line no-console
- console.error("Failed to clear command history:", error);
+ log(`error: failed to clear command history: ${error}`);
}
}
diff --git a/codex-cli/src/utils/storage/save-rollout.ts b/codex-cli/src/utils/storage/save-rollout.ts
index 76d931fd9f..d6b5a49190 100644
--- a/codex-cli/src/utils/storage/save-rollout.ts
+++ b/codex-cli/src/utils/storage/save-rollout.ts
@@ -1,20 +1,19 @@
-/* eslint-disable no-console */
-
import type { ResponseItem } from "openai/resources/responses/responses";
import { loadConfig } from "../config";
+import { log } from "../logger/log.js";
import fs from "fs/promises";
import os from "os";
import path from "path";
const SESSIONS_ROOT = path.join(os.homedir(), ".codex", "sessions");
-async function saveRolloutToHomeSessions(
+async function saveRolloutAsync(
+ sessionId: string,
items: Array,
): Promise {
await fs.mkdir(SESSIONS_ROOT, { recursive: true });
- const sessionId = crypto.randomUUID();
const timestamp = new Date().toISOString();
const ts = timestamp.replace(/[:.]/g, "-").slice(0, 10);
const filename = `rollout-${ts}-${sessionId}.json`;
@@ -39,23 +38,15 @@ async function saveRolloutToHomeSessions(
"utf8",
);
} catch (error) {
- console.error(`Failed to save rollout to ${filePath}: `, error);
+ log(`error: failed to save rollout to ${filePath}: ${error}`);
}
}
-let debounceTimer: NodeJS.Timeout | null = null;
-let pendingItems: Array | null = null;
-
-export function saveRollout(items: Array): void {
- pendingItems = items;
- if (debounceTimer) {
- clearTimeout(debounceTimer);
- }
- debounceTimer = setTimeout(() => {
- if (pendingItems) {
- saveRolloutToHomeSessions(pendingItems).catch(() => {});
- pendingItems = null;
- }
- debounceTimer = null;
- }, 2000);
+export function saveRollout(
+ sessionId: string,
+ items: Array,
+): void {
+ // Best-effort. We also do not log here in case of failure as that should be taken care of
+ // by `saveRolloutAsync` already.
+ saveRolloutAsync(sessionId, items).catch(() => {});
}
diff --git a/codex-cli/src/utils/terminal-chat-utils.ts b/codex-cli/src/utils/terminal-chat-utils.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/codex-cli/src/utils/terminal.ts b/codex-cli/src/utils/terminal.ts
index e8187eba6b..296cbb39b2 100644
--- a/codex-cli/src/utils/terminal.ts
+++ b/codex-cli/src/utils/terminal.ts
@@ -50,6 +50,8 @@ export function clearTerminal(): void {
if (inkRenderer) {
inkRenderer.clear();
}
+ // Also clear scrollback and primary buffer to ensure a truly blank slate
+ process.stdout.write("\x1b[3J\x1b[H\x1b[2J");
}
export function onExit(): void {
diff --git a/codex-cli/tests/__snapshots__/check-updates.test.ts.snap b/codex-cli/tests/__snapshots__/check-updates.test.ts.snap
new file mode 100644
index 0000000000..c9514dd390
--- /dev/null
+++ b/codex-cli/tests/__snapshots__/check-updates.test.ts.snap
@@ -0,0 +1,12 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`checkForUpdates() > renders a box when a newer version exists and no global installer 1`] = `
+"
+ ╭─────────────────────────────────────────────────╮
+ │ │
+ │ Update available! 1.0.0 → 2.0.0. │
+ │ To update, run bun add -g my-pkg to update. │
+ │ │
+ ╰─────────────────────────────────────────────────╯
+"
+`;
diff --git a/codex-cli/tests/agent-project-doc.test.ts b/codex-cli/tests/agent-project-doc.test.ts
index 4b8951e7a0..d421c268c0 100644
--- a/codex-cli/tests/agent-project-doc.test.ts
+++ b/codex-cli/tests/agent-project-doc.test.ts
@@ -50,7 +50,7 @@ vi.mock("openai", () => {
// The AgentLoop pulls these helpers in order to decide whether a command can
// be auto‑approved. None of that matters for this test, so we stub the module
-// with minimal no‑op implementations.
+// with minimal no-op implementations.
vi.mock("../src/approvals.js", () => {
return {
__esModule: true,
diff --git a/codex-cli/tests/approvals.test.ts b/codex-cli/tests/approvals.test.ts
index 7cb0bd3d3e..94daacce00 100644
--- a/codex-cli/tests/approvals.test.ts
+++ b/codex-cli/tests/approvals.test.ts
@@ -11,7 +11,13 @@ describe("canAutoApprove()", () => {
const writeablePaths: Array = [];
const check = (command: ReadonlyArray): SafetyAssessment =>
- canAutoApprove(command, "suggest", writeablePaths, env);
+ canAutoApprove(
+ command,
+ /* workdir */ undefined,
+ "suggest",
+ writeablePaths,
+ env,
+ );
test("simple safe commands", () => {
expect(check(["ls"])).toEqual({
@@ -73,7 +79,7 @@ describe("canAutoApprove()", () => {
test("true command is considered safe", () => {
expect(check(["true"])).toEqual({
type: "auto-approve",
- reason: "No‑op (true)",
+ reason: "No-op (true)",
group: "Utility",
runInSandbox: false,
});
@@ -89,4 +95,56 @@ describe("canAutoApprove()", () => {
expect(check(["cargo", "build"])).toEqual({ type: "ask-user" });
});
+
+ test("find", () => {
+ expect(check(["find", ".", "-name", "file.txt"])).toEqual({
+ type: "auto-approve",
+ reason: "Find files or directories",
+ group: "Searching",
+ runInSandbox: false,
+ });
+
+ // Options that can execute arbitrary commands.
+ expect(
+ check(["find", ".", "-name", "file.txt", "-exec", "rm", "{}", ";"]),
+ ).toEqual({
+ type: "ask-user",
+ });
+ expect(
+ check(["find", ".", "-name", "*.py", "-execdir", "python3", "{}", ";"]),
+ ).toEqual({
+ type: "ask-user",
+ });
+ expect(
+ check(["find", ".", "-name", "file.txt", "-ok", "rm", "{}", ";"]),
+ ).toEqual({
+ type: "ask-user",
+ });
+ expect(
+ check(["find", ".", "-name", "*.py", "-okdir", "python3", "{}", ";"]),
+ ).toEqual({
+ type: "ask-user",
+ });
+
+ // Option that deletes matching files.
+ expect(check(["find", ".", "-delete", "-name", "file.txt"])).toEqual({
+ type: "ask-user",
+ });
+
+ // Options that write pathnames to a file.
+ expect(check(["find", ".", "-fls", "/etc/passwd"])).toEqual({
+ type: "ask-user",
+ });
+ expect(check(["find", ".", "-fprint", "/etc/passwd"])).toEqual({
+ type: "ask-user",
+ });
+ expect(check(["find", ".", "-fprint0", "/etc/passwd"])).toEqual({
+ type: "ask-user",
+ });
+ expect(
+ check(["find", ".", "-fprintf", "/root/suid.txt", "%#m %u %p\n"]),
+ ).toEqual({
+ type: "ask-user",
+ });
+ });
});
diff --git a/codex-cli/tests/check-updates.test.ts b/codex-cli/tests/check-updates.test.ts
new file mode 100644
index 0000000000..75ec8aaf4e
--- /dev/null
+++ b/codex-cli/tests/check-updates.test.ts
@@ -0,0 +1,178 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+import { join } from "node:path";
+import os from "node:os";
+import type { UpdateOptions } from "../src/utils/check-updates";
+import { getLatestVersion } from "fast-npm-meta";
+import { getUserAgent } from "package-manager-detector";
+import {
+ checkForUpdates,
+ renderUpdateCommand,
+} from "../src/utils/check-updates";
+import { detectInstallerByPath } from "../src/utils/package-manager-detector";
+import { CLI_VERSION } from "../src/utils/session";
+
+// In-memory FS mock
+let memfs: Record = {};
+vi.mock("node:fs/promises", async (importOriginal) => {
+ return {
+ ...(await importOriginal()),
+ readFile: async (path: string) => {
+ if (!(path in memfs)) {
+ const err: any = new Error(
+ `ENOENT: no such file or directory, open '${path}'`,
+ );
+ err.code = "ENOENT";
+ throw err;
+ }
+ return memfs[path];
+ },
+ writeFile: async (path: string, data: string) => {
+ memfs[path] = data;
+ },
+ rm: async (path: string) => {
+ delete memfs[path];
+ },
+ };
+});
+
+// Mock package name & CLI version
+const MOCK_PKG = "my-pkg";
+vi.mock("../package.json", () => ({ name: MOCK_PKG }));
+vi.mock("../src/utils/session", () => ({ CLI_VERSION: "1.0.0" }));
+vi.mock("../src/utils/package-manager-detector", async (importOriginal) => {
+ return {
+ ...(await importOriginal()),
+ detectInstallerByPath: vi.fn(),
+ };
+});
+
+// Mock external services
+vi.mock("fast-npm-meta", () => ({ getLatestVersion: vi.fn() }));
+vi.mock("package-manager-detector", () => ({ getUserAgent: vi.fn() }));
+
+describe("renderUpdateCommand()", () => {
+ it.each([
+ [{ manager: "npm", packageName: MOCK_PKG }, `npm install -g ${MOCK_PKG}`],
+ [{ manager: "pnpm", packageName: MOCK_PKG }, `pnpm add -g ${MOCK_PKG}`],
+ [{ manager: "bun", packageName: MOCK_PKG }, `bun add -g ${MOCK_PKG}`],
+ [{ manager: "yarn", packageName: MOCK_PKG }, `yarn global add ${MOCK_PKG}`],
+ [
+ { manager: "deno", packageName: MOCK_PKG },
+ `deno install -g npm:${MOCK_PKG}`,
+ ],
+ ])("%s → command", async (options, cmd) => {
+ expect(renderUpdateCommand(options as UpdateOptions)).toBe(cmd);
+ });
+});
+
+describe("checkForUpdates()", () => {
+ // Use a stable directory under the OS temp
+ const TMP = join(os.tmpdir(), "update-test-memfs");
+ const STATE_PATH = join(TMP, "update-check.json");
+
+ beforeEach(async () => {
+ memfs = {};
+ // Mock CONFIG_DIR to our TMP
+ vi.doMock("../src/utils/config", () => ({ CONFIG_DIR: TMP }));
+
+ // Freeze time so the 24h logic is deterministic
+ vi.useFakeTimers().setSystemTime(new Date("2025-01-01T00:00:00Z"));
+ vi.resetAllMocks();
+ });
+
+ afterEach(async () => {
+ vi.useRealTimers();
+ });
+
+ it("uses global installer when detected, ignoring local agent", async () => {
+ // seed old timestamp
+ const old = new Date("2000-01-01T00:00:00Z").toUTCString();
+ memfs[STATE_PATH] = JSON.stringify({ lastUpdateCheck: old });
+
+ // simulate registry says update available
+ vi.mocked(getLatestVersion).mockResolvedValue({ version: "2.0.0" } as any);
+ // local agent would be npm, but global detection wins
+ vi.mocked(getUserAgent).mockReturnValue("npm");
+ vi.mocked(detectInstallerByPath).mockReturnValue(Promise.resolve("pnpm"));
+
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ await checkForUpdates();
+
+ // should render using `pnpm` (global) rather than `npm`
+ expect(logSpy).toHaveBeenCalledOnce();
+ const output = logSpy.mock.calls.at(0)?.at(0);
+ expect(output).toContain("pnpm add -g"); // global branch used
+ // state updated
+ const newState = JSON.parse(memfs[STATE_PATH]!);
+ expect(newState.lastUpdateCheck).toBe(new Date().toUTCString());
+ });
+
+ it("skips when lastUpdateCheck is still fresh ( {
+ // seed a timestamp 12h ago
+ const recent = new Date(Date.now() - 1000 * 60 * 60 * 12).toUTCString();
+ memfs[STATE_PATH] = JSON.stringify({ lastUpdateCheck: recent });
+
+ const versionSpy = vi.mocked(getLatestVersion);
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ await checkForUpdates();
+
+ expect(versionSpy).not.toHaveBeenCalled();
+ expect(logSpy).not.toHaveBeenCalled();
+ });
+
+ it("does not print when up-to-date", async () => {
+ vi.mocked(getLatestVersion).mockResolvedValue({
+ version: CLI_VERSION,
+ } as any);
+ vi.mocked(getUserAgent).mockReturnValue("npm");
+ vi.mocked(detectInstallerByPath).mockResolvedValue(undefined);
+
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ await checkForUpdates();
+
+ expect(logSpy).not.toHaveBeenCalled();
+ // but state still written
+ const state = JSON.parse(memfs[STATE_PATH]!);
+ expect(state.lastUpdateCheck).toBe(new Date().toUTCString());
+ });
+
+ it("does not print when no manager detected at all", async () => {
+ vi.mocked(getLatestVersion).mockResolvedValue({ version: "2.0.0" } as any);
+ vi.mocked(detectInstallerByPath).mockResolvedValue(undefined);
+ vi.mocked(getUserAgent).mockReturnValue(null);
+
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ await checkForUpdates();
+
+ expect(logSpy).not.toHaveBeenCalled();
+ // state still written
+ const state = JSON.parse(memfs[STATE_PATH]!);
+ expect(state.lastUpdateCheck).toBe(new Date().toUTCString());
+ });
+
+ it("renders a box when a newer version exists and no global installer", async () => {
+ // old timestamp
+ const old = new Date("2000-01-01T00:00:00Z").toUTCString();
+ memfs[STATE_PATH] = JSON.stringify({ lastUpdateCheck: old });
+
+ vi.mocked(getLatestVersion).mockResolvedValue({ version: "2.0.0" } as any);
+ vi.mocked(detectInstallerByPath).mockResolvedValue(undefined);
+ vi.mocked(getUserAgent).mockReturnValue("bun");
+
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ await checkForUpdates();
+
+ expect(logSpy).toHaveBeenCalledOnce();
+ const output = logSpy.mock.calls[0]![0] as string;
+ expect(output).toContain("bun add -g");
+ expect(output).to.matchSnapshot();
+ // state updated
+ const state = JSON.parse(memfs[STATE_PATH]!);
+ expect(state.lastUpdateCheck).toBe(new Date().toUTCString());
+ });
+});
diff --git a/codex-cli/tests/clear-command.test.tsx b/codex-cli/tests/clear-command.test.tsx
new file mode 100644
index 0000000000..bab9b84c9b
--- /dev/null
+++ b/codex-cli/tests/clear-command.test.tsx
@@ -0,0 +1,174 @@
+import React from "react";
+import type { ComponentProps } from "react";
+import { describe, it, expect, vi } from "vitest";
+import { renderTui } from "./ui-test-helpers.js";
+import TerminalChatInput from "../src/components/chat/terminal-chat-input.js";
+import TerminalChatNewInput from "../src/components/chat/terminal-chat-new-input.js";
+import * as TermUtils from "../src/utils/terminal.js";
+
+// -------------------------------------------------------------------------------------------------
+// Helpers
+// -------------------------------------------------------------------------------------------------
+
+async function type(
+ stdin: NodeJS.WritableStream,
+ text: string,
+ flush: () => Promise,
+): Promise {
+ stdin.write(text);
+ await flush();
+}
+
+// -------------------------------------------------------------------------------------------------
+// Tests
+// -------------------------------------------------------------------------------------------------
+
+describe("/clear command", () => {
+ it("invokes clearTerminal and resets context in TerminalChatInput", async () => {
+ const clearSpy = vi
+ .spyOn(TermUtils, "clearTerminal")
+ .mockImplementation(() => {});
+
+ const setItems = vi.fn();
+
+ // Minimal stub of a ResponseItem – cast to bypass exhaustive type checks in this test context
+ const existingItems = [
+ {
+ id: "dummy-1",
+ type: "message",
+ role: "system",
+ content: [{ type: "input_text", text: "Old item" }],
+ },
+ ] as Array;
+
+ const props: ComponentProps = {
+ isNew: false,
+ loading: false,
+ submitInput: () => {},
+ confirmationPrompt: null,
+ explanation: undefined,
+ submitConfirmation: () => {},
+ setLastResponseId: () => {},
+ setItems,
+ contextLeftPercent: 100,
+ openOverlay: () => {},
+ openModelOverlay: () => {},
+ openApprovalOverlay: () => {},
+ openHelpOverlay: () => {},
+ openDiffOverlay: () => {},
+ onCompact: () => {},
+ interruptAgent: () => {},
+ active: true,
+ thinkingSeconds: 0,
+ items: existingItems,
+ };
+
+ const { stdin, flush, cleanup } = renderTui(
+ ,
+ );
+
+ await flush();
+
+ await type(stdin, "/clear", flush);
+ await type(stdin, "\r", flush); // press Enter
+
+ // Allow any asynchronous state updates to propagate
+ await flush();
+
+ expect(clearSpy).toHaveBeenCalledTimes(2);
+ expect(setItems).toHaveBeenCalledTimes(2);
+
+ const stateUpdater = setItems.mock.calls[0]![0];
+ expect(typeof stateUpdater).toBe("function");
+ const newItems = stateUpdater(existingItems);
+ expect(Array.isArray(newItems)).toBe(true);
+ expect(newItems).toHaveLength(2);
+ expect(newItems.at(-1)).toMatchObject({
+ role: "system",
+ type: "message",
+ content: [{ type: "input_text", text: "Terminal cleared" }],
+ });
+
+ cleanup();
+ clearSpy.mockRestore();
+ });
+
+ it("invokes clearTerminal and resets context in TerminalChatNewInput", async () => {
+ const clearSpy = vi
+ .spyOn(TermUtils, "clearTerminal")
+ .mockImplementation(() => {});
+
+ const setItems = vi.fn();
+
+ const props: ComponentProps = {
+ isNew: false,
+ loading: false,
+ submitInput: () => {},
+ confirmationPrompt: null,
+ explanation: undefined,
+ submitConfirmation: () => {},
+ setLastResponseId: () => {},
+ setItems,
+ contextLeftPercent: 100,
+ openOverlay: () => {},
+ openModelOverlay: () => {},
+ openApprovalOverlay: () => {},
+ openHelpOverlay: () => {},
+ openDiffOverlay: () => {},
+ interruptAgent: () => {},
+ active: true,
+ thinkingSeconds: 0,
+ };
+
+ const { stdin, flush, cleanup } = renderTui(
+ ,
+ );
+
+ await flush();
+
+ await type(stdin, "/clear", flush);
+ await type(stdin, "\r", flush); // press Enter
+
+ await flush();
+
+ expect(clearSpy).toHaveBeenCalledTimes(1);
+ expect(setItems).toHaveBeenCalledTimes(1);
+
+ const firstArg = setItems.mock.calls[0]![0];
+ expect(Array.isArray(firstArg)).toBe(true);
+ expect(firstArg).toHaveLength(1);
+ expect(firstArg[0]).toMatchObject({
+ role: "system",
+ type: "message",
+ content: [{ type: "input_text", text: "Terminal cleared" }],
+ });
+
+ cleanup();
+ clearSpy.mockRestore();
+ });
+});
+
+describe("clearTerminal", () => {
+ it("writes escape sequence to stdout", () => {
+ const originalQuiet = process.env["CODEX_QUIET_MODE"];
+ delete process.env["CODEX_QUIET_MODE"];
+
+ process.env["CODEX_QUIET_MODE"] = "0";
+
+ const writeSpy = vi
+ .spyOn(process.stdout, "write")
+ .mockImplementation(() => true);
+
+ TermUtils.clearTerminal();
+
+ expect(writeSpy).toHaveBeenCalledWith("\x1b[3J\x1b[H\x1b[2J");
+
+ writeSpy.mockRestore();
+
+ if (originalQuiet !== undefined) {
+ process.env["CODEX_QUIET_MODE"] = originalQuiet;
+ } else {
+ delete process.env["CODEX_QUIET_MODE"];
+ }
+ });
+});
diff --git a/codex-cli/tests/config.test.tsx b/codex-cli/tests/config.test.tsx
index 024db85354..e94b5b2951 100644
--- a/codex-cli/tests/config.test.tsx
+++ b/codex-cli/tests/config.test.tsx
@@ -1,9 +1,11 @@
import type * as fsType from "fs";
import { loadConfig, saveConfig } from "../src/utils/config.js"; // parent import first
+import { AutoApprovalMode } from "../src/utils/auto-approval-mode.js";
import { tmpdir } from "os";
import { join } from "path";
import { test, expect, beforeEach, afterEach, vi } from "vitest";
+import { providers as defaultProviders } from "../src/utils/providers";
// In‑memory FS store
let memfs: Record = {};
@@ -25,7 +27,7 @@ vi.mock("fs", async () => {
memfs[path] = data;
},
mkdirSync: () => {
- // no‑op in in‑memory store
+ // no-op in in‑memory store
},
rmSync: (path: string) => {
// recursively delete any key under this prefix
@@ -107,3 +109,128 @@ test("loads user instructions + project doc when codex.md is present", () => {
userInstr + "\n\n--- project-doc ---\n\n" + projectDoc,
);
});
+
+test("loads and saves approvalMode correctly", () => {
+ // Setup config with approvalMode
+ memfs[testConfigPath] = JSON.stringify(
+ {
+ model: "mymodel",
+ approvalMode: AutoApprovalMode.AUTO_EDIT,
+ },
+ null,
+ 2,
+ );
+ memfs[testInstructionsPath] = "test instructions";
+
+ // Load config and verify approvalMode
+ const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
+ disableProjectDoc: true,
+ });
+
+ // Check approvalMode was loaded correctly
+ expect(loadedConfig.approvalMode).toBe(AutoApprovalMode.AUTO_EDIT);
+
+ // Modify approvalMode and save
+ const updatedConfig = {
+ ...loadedConfig,
+ approvalMode: AutoApprovalMode.FULL_AUTO,
+ };
+
+ saveConfig(updatedConfig, testConfigPath, testInstructionsPath);
+
+ // Verify saved config contains updated approvalMode
+ expect(memfs[testConfigPath]).toContain(
+ `"approvalMode": "${AutoApprovalMode.FULL_AUTO}"`,
+ );
+
+ // Load again and verify updated value
+ const reloadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
+ disableProjectDoc: true,
+ });
+ expect(reloadedConfig.approvalMode).toBe(AutoApprovalMode.FULL_AUTO);
+});
+
+test("loads and saves providers correctly", () => {
+ // Setup custom providers configuration
+ const customProviders = {
+ openai: {
+ name: "Custom OpenAI",
+ baseURL: "https://custom-api.openai.com/v1",
+ envKey: "CUSTOM_OPENAI_API_KEY",
+ },
+ anthropic: {
+ name: "Anthropic",
+ baseURL: "https://api.anthropic.com",
+ envKey: "ANTHROPIC_API_KEY",
+ },
+ };
+
+ // Create config with providers
+ const testConfig = {
+ model: "test-model",
+ provider: "anthropic",
+ providers: customProviders,
+ instructions: "test instructions",
+ notify: false,
+ };
+
+ // Save the config
+ saveConfig(testConfig, testConfigPath, testInstructionsPath);
+
+ // Verify saved config contains providers
+ expect(memfs[testConfigPath]).toContain(`"providers"`);
+ expect(memfs[testConfigPath]).toContain(`"Custom OpenAI"`);
+ expect(memfs[testConfigPath]).toContain(`"Anthropic"`);
+ expect(memfs[testConfigPath]).toContain(`"provider": "anthropic"`);
+
+ // Load config and verify providers were loaded correctly
+ const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
+ disableProjectDoc: true,
+ });
+
+ // Check providers were loaded correctly
+ expect(loadedConfig.provider).toBe("anthropic");
+ expect(loadedConfig.providers).toEqual({
+ ...defaultProviders,
+ ...customProviders,
+ });
+
+ // Test merging with built-in providers
+ // Create a config with only one custom provider
+ const partialProviders = {
+ customProvider: {
+ name: "Custom Provider",
+ baseURL: "https://custom-api.example.com",
+ envKey: "CUSTOM_API_KEY",
+ },
+ };
+
+ const partialConfig = {
+ model: "test-model",
+ provider: "customProvider",
+ providers: partialProviders,
+ instructions: "test instructions",
+ notify: false,
+ };
+
+ // Save the partial config
+ saveConfig(partialConfig, testConfigPath, testInstructionsPath);
+
+ // Load config and verify providers were merged with built-in providers
+ const mergedConfig = loadConfig(testConfigPath, testInstructionsPath, {
+ disableProjectDoc: true,
+ });
+
+ // Check providers is defined
+ expect(mergedConfig.providers).toBeDefined();
+
+ // Use bracket notation to access properties
+ if (mergedConfig.providers) {
+ expect(mergedConfig.providers["customProvider"]).toBeDefined();
+ expect(mergedConfig.providers["customProvider"]).toEqual(
+ partialProviders.customProvider,
+ );
+ // Built-in providers should still be there (like openai)
+ expect(mergedConfig.providers["openai"]).toBeDefined();
+ }
+});
diff --git a/codex-cli/tests/create-truncating-collector.test.ts b/codex-cli/tests/create-truncating-collector.test.ts
new file mode 100644
index 0000000000..ad3dee558f
--- /dev/null
+++ b/codex-cli/tests/create-truncating-collector.test.ts
@@ -0,0 +1,55 @@
+import { PassThrough } from "stream";
+import { once } from "events";
+import { describe, it, expect } from "vitest";
+import { createTruncatingCollector } from "../src/utils/agent/sandbox/create-truncating-collector.js";
+
+describe("createTruncatingCollector", () => {
+ it("collects data under limits without truncation", async () => {
+ const stream = new PassThrough();
+ const collector = createTruncatingCollector(stream, 100, 10);
+ const data = "line1\nline2\n";
+ stream.end(Buffer.from(data));
+ await once(stream, "end");
+ expect(collector.getString()).toBe(data);
+ expect(collector.hit).toBe(false);
+ });
+
+ it("truncates data over byte limit", async () => {
+ const stream = new PassThrough();
+ const collector = createTruncatingCollector(stream, 5, 100);
+ stream.end(Buffer.from("hello world"));
+ await once(stream, "end");
+ expect(collector.getString()).toBe("hello");
+ expect(collector.hit).toBe(true);
+ });
+
+ it("truncates data over line limit", async () => {
+ const stream = new PassThrough();
+ const collector = createTruncatingCollector(stream, 1000, 2);
+ const data = "a\nb\nc\nd\n";
+ stream.end(Buffer.from(data));
+ await once(stream, "end");
+ expect(collector.getString()).toBe("a\nb\n");
+ expect(collector.hit).toBe(true);
+ });
+
+ it("stops collecting after limit is hit across multiple writes", async () => {
+ const stream = new PassThrough();
+ const collector = createTruncatingCollector(stream, 10, 2);
+ stream.write(Buffer.from("1\n"));
+ stream.write(Buffer.from("2\n3\n4\n"));
+ stream.end();
+ await once(stream, "end");
+ expect(collector.getString()).toBe("1\n2\n");
+ expect(collector.hit).toBe(true);
+ });
+
+ it("handles zero limits", async () => {
+ const stream = new PassThrough();
+ const collector = createTruncatingCollector(stream, 0, 0);
+ stream.end(Buffer.from("anything\n"));
+ await once(stream, "end");
+ expect(collector.getString()).toBe("");
+ expect(collector.hit).toBe(true);
+ });
+});
diff --git a/codex-cli/tests/exec-apply-patch.test.ts b/codex-cli/tests/exec-apply-patch.test.ts
new file mode 100644
index 0000000000..d28411fa78
--- /dev/null
+++ b/codex-cli/tests/exec-apply-patch.test.ts
@@ -0,0 +1,44 @@
+import { execApplyPatch } from "../src/utils/agent/exec.js";
+import fs from "fs";
+import os from "os";
+import path from "path";
+import { test, expect } from "vitest";
+
+/**
+ * This test verifies that `execApplyPatch()` is able to add a new file whose
+ * parent directory does not yet exist. Prior to the fix, the call would throw
+ * because `fs.writeFileSync()` could not create intermediate directories. The
+ * test creates an isolated temporary directory to avoid polluting the project
+ * workspace.
+ */
+test("execApplyPatch creates missing directories when adding a file", () => {
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "apply-patch-test-"));
+
+ // Ensure we start from a clean slate.
+ const nestedFileRel = path.join("foo", "bar", "baz.txt");
+ const nestedFileAbs = path.join(tmpDir, nestedFileRel);
+ expect(fs.existsSync(nestedFileAbs)).toBe(false);
+
+ const patch = `*** Begin Patch\n*** Add File: ${nestedFileRel}\n+hello new world\n*** End Patch`;
+
+ // Run execApplyPatch() with cwd switched to tmpDir so that the relative
+ // path in the patch is resolved inside the temporary location.
+ const prevCwd = process.cwd();
+ try {
+ process.chdir(tmpDir);
+
+ const result = execApplyPatch(patch);
+ expect(result.exitCode).toBe(0);
+ expect(result.stderr).toBe("");
+ } finally {
+ process.chdir(prevCwd);
+ }
+
+ // The file (and its parent directories) should have been created with the
+ // expected contents.
+ const fileContents = fs.readFileSync(nestedFileAbs, "utf8");
+ expect(fileContents).toBe("hello new world");
+
+ // Cleanup to keep tmpdir tidy.
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+});
diff --git a/codex-cli/tests/file-system-suggestions.test.ts b/codex-cli/tests/file-system-suggestions.test.ts
new file mode 100644
index 0000000000..2477306c0d
--- /dev/null
+++ b/codex-cli/tests/file-system-suggestions.test.ts
@@ -0,0 +1,73 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import fs from "fs";
+import os from "os";
+import path from "path";
+import { getFileSystemSuggestions } from "../src/utils/file-system-suggestions";
+
+vi.mock("fs");
+vi.mock("os");
+
+describe("getFileSystemSuggestions", () => {
+ const mockFs = fs as unknown as {
+ readdirSync: ReturnType;
+ statSync: ReturnType;
+ };
+
+ const mockOs = os as unknown as {
+ homedir: ReturnType;
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("returns empty array for empty prefix", () => {
+ expect(getFileSystemSuggestions("")).toEqual([]);
+ });
+
+ it("expands ~ to home directory", () => {
+ mockOs.homedir = vi.fn(() => "/home/testuser");
+ mockFs.readdirSync = vi.fn(() => ["file1.txt", "docs"]);
+ mockFs.statSync = vi.fn((p) => ({
+ isDirectory: () => path.basename(p) === "docs",
+ }));
+
+ const result = getFileSystemSuggestions("~/");
+
+ expect(mockFs.readdirSync).toHaveBeenCalledWith("/home/testuser");
+ expect(result).toEqual([
+ path.join("/home/testuser", "file1.txt"),
+ path.join("/home/testuser", "docs" + path.sep),
+ ]);
+ });
+
+ it("filters by prefix if not a directory", () => {
+ mockFs.readdirSync = vi.fn(() => ["abc.txt", "abd.txt", "xyz.txt"]);
+ mockFs.statSync = vi.fn((p) => ({
+ isDirectory: () => p.includes("abd"),
+ }));
+
+ const result = getFileSystemSuggestions("a");
+ expect(result).toEqual(["abc.txt", "abd.txt/"]);
+ });
+
+ it("handles errors gracefully", () => {
+ mockFs.readdirSync = vi.fn(() => {
+ throw new Error("failed");
+ });
+
+ const result = getFileSystemSuggestions("some/path");
+ expect(result).toEqual([]);
+ });
+
+ it("normalizes relative path", () => {
+ mockFs.readdirSync = vi.fn(() => ["foo", "bar"]);
+ mockFs.statSync = vi.fn((_p) => ({
+ isDirectory: () => true,
+ }));
+
+ const result = getFileSystemSuggestions("./");
+ expect(result).toContain("foo/");
+ expect(result).toContain("bar/");
+ });
+});
diff --git a/codex-cli/tests/fixed-requires-shell.test.ts b/codex-cli/tests/fixed-requires-shell.test.ts
new file mode 100644
index 0000000000..dd1058dbc4
--- /dev/null
+++ b/codex-cli/tests/fixed-requires-shell.test.ts
@@ -0,0 +1,38 @@
+import { describe, it, expect } from "vitest";
+import { parse } from "shell-quote";
+
+// The fixed requiresShell function
+function requiresShell(cmd: Array): boolean {
+ // If the command is a single string that contains shell operators,
+ // it needs to be run with shell: true
+ if (cmd.length === 1 && cmd[0] !== undefined) {
+ const tokens = parse(cmd[0]) as Array;
+ return tokens.some((token) => typeof token === "object" && "op" in token);
+ }
+
+ // If the command is split into multiple arguments, we don't need shell: true
+ // even if one of the arguments is a shell operator like '|'
+ return false;
+}
+
+describe("fixed requiresShell function", () => {
+ it("should detect pipe in a single argument", () => {
+ const cmd = ['grep -n "finally:" some-file | head'];
+ expect(requiresShell(cmd)).toBe(true);
+ });
+
+ it("should not detect pipe in separate arguments", () => {
+ const cmd = ["grep", "-n", "finally:", "some-file", "|", "head"];
+ expect(requiresShell(cmd)).toBe(false);
+ });
+
+ it("should handle other shell operators in a single argument", () => {
+ const cmd = ["echo hello && echo world"];
+ expect(requiresShell(cmd)).toBe(true);
+ });
+
+ it("should not enable shell for normal commands", () => {
+ const cmd = ["ls", "-la"];
+ expect(requiresShell(cmd)).toBe(false);
+ });
+});
diff --git a/codex-cli/tests/history-overlay.test.tsx b/codex-cli/tests/history-overlay.test.tsx
new file mode 100644
index 0000000000..1ae2a2d2dd
--- /dev/null
+++ b/codex-cli/tests/history-overlay.test.tsx
@@ -0,0 +1,350 @@
+/* -------------------------------------------------------------------------- *
+ * Tests for the HistoryOverlay component and its formatHistoryForDisplay utility function
+ *
+ * The component displays a list of commands and files from the chat history.
+ * It supports two modes:
+ * - Command mode: shows all commands and user messages
+ * - File mode: shows all files that were touched
+ *
+ * The formatHistoryForDisplay function processes ResponseItems to extract:
+ * - Commands: User messages and function calls
+ * - Files: Paths referenced in commands or function calls
+ * -------------------------------------------------------------------------- */
+
+import { describe, it, expect, vi } from "vitest";
+import { render } from "ink-testing-library";
+import React from "react";
+import type {
+ ResponseInputMessageItem,
+ ResponseFunctionToolCallItem,
+} from "openai/resources/responses/responses.mjs";
+import HistoryOverlay from "../src/components/history-overlay";
+
+// ---------------------------------------------------------------------------
+// Module mocks *must* be registered *before* the module under test is imported
+// so that Vitest can replace the dependency during evaluation.
+// ---------------------------------------------------------------------------
+
+// Mock ink's useInput to capture keyboard handlers
+let keyboardHandler: ((input: string, key: any) => void) | undefined;
+vi.mock("ink", async () => {
+ const actual = await vi.importActual("ink");
+ return {
+ ...actual,
+ useInput: (handler: (input: string, key: any) => void) => {
+ keyboardHandler = handler;
+ },
+ };
+});
+
+// ---------------------------------------------------------------------------
+// Test Helpers
+// ---------------------------------------------------------------------------
+
+function createUserMessage(content: string): ResponseInputMessageItem {
+ return {
+ type: "message",
+ role: "user",
+ id: `msg_${Math.random().toString(36).slice(2)}`,
+ content: [{ type: "input_text", text: content }],
+ };
+}
+
+function createFunctionCall(
+ name: string,
+ args: unknown,
+): ResponseFunctionToolCallItem {
+ return {
+ type: "function_call",
+ name,
+ id: `fn_${Math.random().toString(36).slice(2)}`,
+ call_id: `call_${Math.random().toString(36).slice(2)}`,
+ arguments: JSON.stringify(args),
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+describe("HistoryOverlay", () => {
+ describe("command mode", () => {
+ it("displays user messages", () => {
+ const items = [createUserMessage("hello"), createUserMessage("world")];
+ const { lastFrame } = render(
+ ,
+ );
+ const frame = lastFrame();
+ expect(frame).toContain("hello");
+ expect(frame).toContain("world");
+ });
+
+ it("displays shell commands", () => {
+ const items = [
+ createFunctionCall("shell", { cmd: ["ls", "-la"] }),
+ createFunctionCall("shell", { cmd: ["pwd"] }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+ const frame = lastFrame();
+ expect(frame).toContain("ls -la");
+ expect(frame).toContain("pwd");
+ });
+
+ it("displays file operations", () => {
+ const items = [createFunctionCall("read_file", { path: "test.txt" })];
+ const { lastFrame } = render(
+ ,
+ );
+ const frame = lastFrame();
+ expect(frame).toContain("read_file test.txt");
+ });
+
+ it("displays patch operations", () => {
+ const items = [
+ createFunctionCall("shell", {
+ cmd: [
+ "apply_patch",
+ "*** Begin Patch\n--- a/src/file1.txt\n+++ b/src/file1.txt\n@@ -1,5 +1,5 @@\n-const x = 1;\n+const x = 2;\n",
+ ],
+ }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+
+ // Verify patch is displayed in command mode
+ let frame = lastFrame();
+ expect(frame).toContain("apply_patch");
+ expect(frame).toContain("src/file1.txt");
+
+ // Verify file is extracted in file mode
+ keyboardHandler?.("f", {});
+ frame = lastFrame();
+ expect(frame).toContain("src/file1.txt");
+ });
+
+ it("displays mixed content in chronological order", () => {
+ const items = [
+ createUserMessage("first message"),
+ createFunctionCall("shell", { cmd: ["echo", "hello"] }),
+ createUserMessage("second message"),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+ const frame = lastFrame();
+ expect(frame).toContain("first message");
+ expect(frame).toContain("echo hello");
+ expect(frame).toContain("second message");
+ });
+
+ it("truncates long user messages", () => {
+ const shortMessage = "Hello";
+ const longMessage =
+ "This is a very long message that should be truncated because it exceeds the maximum length of 120 characters. We need to make sure it gets properly truncated with the right prefix and ellipsis.";
+ const items = [
+ createUserMessage(shortMessage),
+ createUserMessage(longMessage),
+ ];
+
+ const { lastFrame } = render(
+ ,
+ );
+ const frame = lastFrame()!;
+
+ // Short message should have the > prefix
+ expect(frame).toContain(`> ${shortMessage}`);
+
+ // Long message should be truncated and contain:
+ // 1. The > prefix
+ expect(frame).toContain("> This is a very long message");
+ // 2. An ellipsis indicating truncation
+ expect(frame).toContain("…");
+ // 3. Not contain the full message
+ expect(frame).not.toContain(longMessage);
+
+ // Find the truncated message line
+ const lines = frame.split("\n");
+ const truncatedLine = lines.find((line) =>
+ line.includes("This is a very long message"),
+ )!;
+ // Verify it's not too long (allowing for some UI elements)
+ expect(truncatedLine.trim().length).toBeLessThan(150);
+ });
+ });
+
+ describe("file mode", () => {
+ it("displays files from shell commands", () => {
+ const items = [
+ createFunctionCall("shell", { cmd: ["cat", "/path/to/file"] }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+
+ // Switch to file mode
+ keyboardHandler?.("f", {});
+ const frame = lastFrame();
+ expect(frame).toContain("Files touched");
+ expect(frame).toContain("/path/to/file");
+ });
+
+ it("displays files from read operations", () => {
+ const items = [
+ createFunctionCall("read_file", { path: "/path/to/file" }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+
+ // Switch to file mode
+ keyboardHandler?.("f", {});
+ const frame = lastFrame();
+ expect(frame).toContain("Files touched");
+ expect(frame).toContain("/path/to/file");
+ });
+
+ it("displays files from patches", () => {
+ const items = [
+ createFunctionCall("shell", {
+ cmd: [
+ "apply_patch",
+ "*** Begin Patch\n--- a/src/file1.txt\n+++ b/src/file1.txt\n@@ -1,5 +1,5 @@\n-const x = 1;\n+const x = 2;\n",
+ ],
+ }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+
+ // Switch to file mode
+ keyboardHandler?.("f", {});
+ const frame = lastFrame();
+ expect(frame).toContain("Files touched");
+ expect(frame).toContain("src/file1.txt");
+ });
+ });
+
+ describe("keyboard interaction", () => {
+ it("handles mode switching with 'c' and 'f' keys", () => {
+ const items = [
+ createUserMessage("hello"),
+ createFunctionCall("shell", { cmd: ["cat", "src/test.txt"] }),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+
+ // Initial state (command mode)
+ let frame = lastFrame();
+ expect(frame).toContain("Commands run");
+ expect(frame).toContain("hello");
+ expect(frame).toContain("cat src/test.txt");
+
+ // Switch to files mode
+ keyboardHandler?.("f", {});
+ frame = lastFrame();
+ expect(frame).toContain("Files touched");
+ expect(frame).toContain("src/test.txt");
+
+ // Switch back to commands mode
+ keyboardHandler?.("c", {});
+ frame = lastFrame();
+ expect(frame).toContain("Commands run");
+ expect(frame).toContain("hello");
+ expect(frame).toContain("cat src/test.txt");
+ });
+
+ it("handles escape key", () => {
+ const onExit = vi.fn();
+ render( );
+
+ keyboardHandler?.("", { escape: true });
+ expect(onExit).toHaveBeenCalled();
+ });
+
+ it("handles arrow keys for navigation", () => {
+ const items = [createUserMessage("first"), createUserMessage("second")];
+ const { lastFrame } = render(
+ ,
+ );
+
+ // Initial state shows first item selected
+ let frame = lastFrame();
+ expect(frame).toContain("› > first");
+ expect(frame).not.toContain("› > second");
+
+ // Move down - second item should be selected
+ keyboardHandler?.("", { downArrow: true });
+ frame = lastFrame();
+ expect(frame).toContain("› > second");
+ expect(frame).not.toContain("› > first");
+
+ // Move up - first item should be selected again
+ keyboardHandler?.("", { upArrow: true });
+ frame = lastFrame();
+ expect(frame).toContain("› > first");
+ expect(frame).not.toContain("› > second");
+ });
+
+ it("handles page up/down navigation", () => {
+ const items = Array.from({ length: 12 }, (_, i) =>
+ createUserMessage(`message ${i + 1}`),
+ );
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ // Initial position - first message selected
+ let frame = lastFrame();
+ expect(frame).toMatch(/│ › > message 1\s+│/); // message 1 should be selected
+ expect(frame).toMatch(/│ {3}> message 11\s+│/); // message 11 should be visible but not selected
+
+ // Page down moves by 10 - message 11 should be selected
+ keyboardHandler?.("", { pageDown: true });
+ frame = lastFrame();
+ expect(frame).toMatch(/│ {3}> message 1\s+│/); // message 1 should be visible but not selected
+ expect(frame).toMatch(/│ › > message 11\s+│/); // message 11 should be selected
+ });
+
+ it("handles vim-style navigation", () => {
+ const items = [
+ createUserMessage("first"),
+ createUserMessage("second"),
+ createUserMessage("third"),
+ ];
+ const { lastFrame } = render(
+ ,
+ );
+
+ // Initial state should show first item selected
+ let frame = lastFrame();
+ expect(frame).toContain("› > first");
+ expect(frame).not.toContain("› > third"); // Make sure third is not selected initially
+
+ // Test G to jump to end - third should be selected
+ keyboardHandler?.("G", {});
+ frame = lastFrame();
+ expect(frame).toContain("› > third");
+
+ // Test g to jump to beginning - first should be selected again
+ keyboardHandler?.("g", {});
+ frame = lastFrame();
+ expect(frame).toContain("› > first");
+ });
+ });
+
+ describe("error handling", () => {
+ it("handles empty or invalid items", () => {
+ const items = [{ type: "invalid" } as any, null as any, undefined as any];
+ const { lastFrame } = render(
+ ,
+ );
+ // Should render without errors
+ expect(lastFrame()).toBeTruthy();
+ });
+ });
+});
diff --git a/codex-cli/tests/input-utils.test.ts b/codex-cli/tests/input-utils.test.ts
new file mode 100644
index 0000000000..780a384240
--- /dev/null
+++ b/codex-cli/tests/input-utils.test.ts
@@ -0,0 +1,47 @@
+import { describe, it, expect, vi } from "vitest";
+import fs from "fs/promises";
+import { createInputItem } from "../src/utils/input-utils.js";
+
+describe("createInputItem", () => {
+ it("returns only text when no images provided", async () => {
+ const result = await createInputItem("hello", []);
+ expect(result).toEqual({
+ role: "user",
+ type: "message",
+ content: [{ type: "input_text", text: "hello" }],
+ });
+ });
+
+ it("includes image content for existing file", async () => {
+ const fakeBuffer = Buffer.from("fake image content");
+ const readSpy = vi
+ .spyOn(fs, "readFile")
+ .mockResolvedValue(fakeBuffer as any);
+ const result = await createInputItem("hello", ["dummy-path"]);
+ const expectedUrl = `data:application/octet-stream;base64,${fakeBuffer.toString(
+ "base64",
+ )}`;
+ expect(result.role).toBe("user");
+ expect(result.type).toBe("message");
+ expect(result.content.length).toBe(2);
+ const [textItem, imageItem] = result.content;
+ expect(textItem).toEqual({ type: "input_text", text: "hello" });
+ expect(imageItem).toEqual({
+ type: "input_image",
+ detail: "auto",
+ image_url: expectedUrl,
+ });
+ readSpy.mockRestore();
+ });
+
+ it("falls back to missing image text for non-existent file", async () => {
+ const filePath = "tests/__fixtures__/does-not-exist.png";
+ const result = await createInputItem("hello", [filePath]);
+ expect(result.content.length).toBe(2);
+ const fallbackItem = result.content[1];
+ expect(fallbackItem).toEqual({
+ type: "input_text",
+ text: "[missing image: does-not-exist.png]",
+ });
+ });
+});
diff --git a/codex-cli/tests/model-info.test.ts b/codex-cli/tests/model-info.test.ts
new file mode 100644
index 0000000000..9626744d29
--- /dev/null
+++ b/codex-cli/tests/model-info.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, test } from "vitest";
+import { openAiModelInfo } from "../src/utils/model-info";
+
+describe("Model Info", () => {
+ test("supportedModelInfo contains expected models", () => {
+ expect(openAiModelInfo).toHaveProperty("gpt-4o");
+ expect(openAiModelInfo).toHaveProperty("gpt-4.1");
+ expect(openAiModelInfo).toHaveProperty("o3");
+ });
+
+ test("model info entries have required properties", () => {
+ Object.entries(openAiModelInfo).forEach(([_, info]) => {
+ expect(info).toHaveProperty("label");
+ expect(info).toHaveProperty("maxContextLength");
+ expect(typeof info.label).toBe("string");
+ expect(typeof info.maxContextLength).toBe("number");
+ });
+ });
+});
diff --git a/codex-cli/tests/model-utils-network-error.test.ts b/codex-cli/tests/model-utils-network-error.test.ts
index f67b4fc4db..537e7fdb35 100644
--- a/codex-cli/tests/model-utils-network-error.test.ts
+++ b/codex-cli/tests/model-utils-network-error.test.ts
@@ -44,7 +44,7 @@ describe("model-utils – offline resilience", () => {
"../src/utils/model-utils.js"
);
- const supported = await isModelSupportedForResponses("o4-mini");
+ const supported = await isModelSupportedForResponses("openai", "o4-mini");
expect(supported).toBe(true);
});
@@ -63,8 +63,11 @@ describe("model-utils – offline resilience", () => {
"../src/utils/model-utils.js"
);
- // Should resolve true despite the network failure
- const supported = await isModelSupportedForResponses("some-model");
+ // Should resolve true despite the network failure.
+ const supported = await isModelSupportedForResponses(
+ "openai",
+ "some-model",
+ );
expect(supported).toBe(true);
});
});
diff --git a/codex-cli/tests/model-utils.test.ts b/codex-cli/tests/model-utils.test.ts
new file mode 100644
index 0000000000..6dd523827c
--- /dev/null
+++ b/codex-cli/tests/model-utils.test.ts
@@ -0,0 +1,78 @@
+import { describe, test, expect } from "vitest";
+import {
+ calculateContextPercentRemaining,
+ maxTokensForModel,
+} from "../src/utils/model-utils";
+import { openAiModelInfo } from "../src/utils/model-info";
+import type { ResponseItem } from "openai/resources/responses/responses.mjs";
+
+describe("Model Utils", () => {
+ describe("openAiModelInfo", () => {
+ test("model info entries have required properties", () => {
+ Object.entries(openAiModelInfo).forEach(([_, info]) => {
+ expect(info).toHaveProperty("label");
+ expect(info).toHaveProperty("maxContextLength");
+ expect(typeof info.label).toBe("string");
+ expect(typeof info.maxContextLength).toBe("number");
+ });
+ });
+ });
+
+ describe("maxTokensForModel", () => {
+ test("returns correct token limit for known models", () => {
+ const knownModel = "gpt-4o";
+ const expectedTokens = openAiModelInfo[knownModel].maxContextLength;
+ expect(maxTokensForModel(knownModel)).toBe(expectedTokens);
+ });
+
+ test("handles models with size indicators in their names", () => {
+ expect(maxTokensForModel("some-model-32k")).toBe(32000);
+ expect(maxTokensForModel("some-model-16k")).toBe(16000);
+ expect(maxTokensForModel("some-model-8k")).toBe(8000);
+ expect(maxTokensForModel("some-model-4k")).toBe(4000);
+ });
+
+ test("defaults to 128k for unknown models not in the registry", () => {
+ expect(maxTokensForModel("completely-unknown-model")).toBe(128000);
+ });
+ });
+
+ describe("calculateContextPercentRemaining", () => {
+ test("returns 100% for empty items", () => {
+ const result = calculateContextPercentRemaining([], "gpt-4o");
+ expect(result).toBe(100);
+ });
+
+ test("calculates percentage correctly for non-empty items", () => {
+ const mockItems: Array = [
+ {
+ id: "test-id",
+ type: "message",
+ role: "user",
+ status: "completed",
+ content: [
+ {
+ type: "input_text",
+ text: "A".repeat(
+ openAiModelInfo["gpt-4o"].maxContextLength * 0.25 * 4,
+ ),
+ },
+ ],
+ } as ResponseItem,
+ ];
+
+ const result = calculateContextPercentRemaining(mockItems, "gpt-4o");
+ expect(result).toBeCloseTo(75, 0);
+ });
+
+ test("handles models that are not in the registry", () => {
+ const mockItems: Array = [];
+
+ const result = calculateContextPercentRemaining(
+ mockItems,
+ "unknown-model",
+ );
+ expect(result).toBe(100);
+ });
+ });
+});
diff --git a/codex-cli/tests/multiline-history-behavior.test.tsx b/codex-cli/tests/multiline-history-behavior.test.tsx
index cada52ddab..ee46d3d5c1 100644
--- a/codex-cli/tests/multiline-history-behavior.test.tsx
+++ b/codex-cli/tests/multiline-history-behavior.test.tsx
@@ -57,7 +57,7 @@ async function type(
await flush();
}
-/** Build a set of no‑op callbacks so renders with minimal
+/** Build a set of no-op callbacks so renders with minimal
* scaffolding.
*/
function stubProps(): any {
diff --git a/codex-cli/tests/multiline-shift-enter-mod1.test.tsx b/codex-cli/tests/multiline-shift-enter-mod1.test.tsx
new file mode 100644
index 0000000000..d309756cfb
--- /dev/null
+++ b/codex-cli/tests/multiline-shift-enter-mod1.test.tsx
@@ -0,0 +1,49 @@
+// Regression test: Terminals with modifyOtherKeys=1 emit CSI~ sequence for
+// Shift+Enter: ESC [ 27 ; mod ; 13 ~. The editor must treat Shift+Enter as
+// newline (without submitting) and Ctrl+Enter as submit.
+
+import { renderTui } from "./ui-test-helpers.js";
+import MultilineTextEditor from "../src/components/chat/multiline-editor.js";
+import * as React from "react";
+import { describe, it, expect, vi } from "vitest";
+
+async function type(
+ stdin: NodeJS.WritableStream,
+ text: string,
+ flush: () => Promise,
+) {
+ stdin.write(text);
+ await flush();
+}
+
+describe("MultilineTextEditor – Shift+Enter with modifyOtherKeys=1", () => {
+ it("inserts newline, does NOT submit", async () => {
+ const onSubmit = vi.fn();
+
+ const { stdin, lastFrameStripped, flush, cleanup } = renderTui(
+ React.createElement(MultilineTextEditor, {
+ height: 5,
+ width: 20,
+ initialText: "",
+ onSubmit,
+ }),
+ );
+
+ await flush();
+
+ await type(stdin, "abc", flush);
+ // Shift+Enter => ESC [27;2;13~
+ await type(stdin, "\u001B[27;2;13~", flush);
+ await type(stdin, "def", flush);
+
+ const frame = lastFrameStripped();
+ expect(frame).toMatch(/abc/);
+ expect(frame).toMatch(/def/);
+ // newline inserted -> at least 2 lines
+ expect(frame.split("\n").length).toBeGreaterThanOrEqual(2);
+
+ expect(onSubmit).not.toHaveBeenCalled();
+
+ cleanup();
+ });
+});
diff --git a/codex-cli/tests/package-manager-detector.test.ts b/codex-cli/tests/package-manager-detector.test.ts
new file mode 100644
index 0000000000..1e80ea58cd
--- /dev/null
+++ b/codex-cli/tests/package-manager-detector.test.ts
@@ -0,0 +1,66 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
+import which from "which";
+import { detectInstallerByPath } from "../src/utils/package-manager-detector";
+import { execFileSync } from "node:child_process";
+
+vi.mock("which", () => ({
+ default: { sync: vi.fn() },
+}));
+vi.mock("node:child_process", () => ({ execFileSync: vi.fn() }));
+
+describe("detectInstallerByPath()", () => {
+ const originalArgv = process.argv;
+ const fakeBinDirs = {
+ // `npm prefix -g` returns the global “prefix” (we’ll add `/bin` when detecting)
+ npm: "/usr/local",
+ pnpm: "/home/user/.local/share/pnpm/bin",
+ bun: "/Users/test/.bun/bin",
+ } as const;
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ // Pretend each manager binary is on PATH:
+ vi.mocked(which.sync).mockImplementation(() => "/fake/path");
+
+ vi.mocked(execFileSync).mockImplementation(
+ (
+ cmd: string,
+ _args: ReadonlyArray = [],
+ _options: unknown,
+ ): string => {
+ return fakeBinDirs[cmd as keyof typeof fakeBinDirs];
+ },
+ );
+ });
+
+ afterEach(() => {
+ // Restore the real argv so tests don’t leak
+ process.argv = originalArgv;
+ });
+
+ it.each(Object.entries(fakeBinDirs))(
+ "detects %s when invoked from its global-bin",
+ async (manager, binDir) => {
+ // Simulate the shim living under that binDir
+ process.argv =
+ manager === "npm"
+ ? [process.argv[0]!, `${binDir}/bin/my-cli`]
+ : [process.argv[0]!, `${binDir}/my-cli`];
+ const detected = await detectInstallerByPath();
+ expect(detected).toBe(manager);
+ },
+ );
+
+ it("returns undefined if argv[1] is missing", async () => {
+ process.argv = [process.argv[0]!];
+ expect(await detectInstallerByPath()).toBeUndefined();
+ expect(execFileSync).not.toHaveBeenCalled();
+ });
+
+ it("returns undefined if shim isn't in any manager's bin", async () => {
+ // stub execFileSync to some other dirs
+ vi.mocked(execFileSync).mockImplementation(() => "/some/other/dir");
+ process.argv = [process.argv[0]!, "/home/user/.node_modules/.bin/my-cli"];
+ expect(await detectInstallerByPath()).toBeUndefined();
+ });
+});
diff --git a/codex-cli/tests/pipe-command.test.ts b/codex-cli/tests/pipe-command.test.ts
new file mode 100644
index 0000000000..652fee99c3
--- /dev/null
+++ b/codex-cli/tests/pipe-command.test.ts
@@ -0,0 +1,19 @@
+import { describe, it, expect } from "vitest";
+import { parse } from "shell-quote";
+
+/* eslint-disable no-console */
+
+describe("shell-quote parse with pipes", () => {
+ it("should correctly parse a command with a pipe", () => {
+ const cmd = 'grep -n "finally:" some-file | head';
+ const tokens = parse(cmd);
+ console.log("Parsed tokens:", JSON.stringify(tokens, null, 2));
+
+ // Check if any token has an 'op' property
+ const hasOpToken = tokens.some(
+ (token) => typeof token === "object" && "op" in token,
+ );
+
+ expect(hasOpToken).toBe(true);
+ });
+});
diff --git a/codex-cli/tests/raw-exec-process-group.test.ts b/codex-cli/tests/raw-exec-process-group.test.ts
index a7515946cf..8dfc282129 100644
--- a/codex-cli/tests/raw-exec-process-group.test.ts
+++ b/codex-cli/tests/raw-exec-process-group.test.ts
@@ -6,7 +6,6 @@ import { exec as rawExec } from "../src/utils/agent/sandbox/raw-exec.js";
// the direct child. The original logic only sent `SIGTERM` to the immediate
// child which meant that grandchildren (for instance when running through a
// `bash -c` wrapper) were left running and turned into "zombie" processes.
-
// Strategy:
// 1. Start a Bash shell that spawns a long‑running `sleep`, prints the PID
// of that `sleep`, and then waits forever. This guarantees we can later
@@ -15,7 +14,6 @@ import { exec as rawExec } from "../src/utils/agent/sandbox/raw-exec.js";
// 3. After `rawExec()` resolves we probe the previously printed PID with
// `process.kill(pid, 0)`. If the call throws `ESRCH` the process no
// longer exists – the desired outcome. Otherwise the test fails.
-
// The negative‑PID process‑group trick employed by the fixed implementation is
// POSIX‑only. On Windows we skip the test.
@@ -26,49 +24,59 @@ describe("rawExec – abort kills entire process group", () => {
}
const abortController = new AbortController();
-
// Bash script: spawn `sleep 30` in background, print its PID, then wait.
const script = "sleep 30 & pid=$!; echo $pid; wait $pid";
const cmd = ["bash", "-c", script];
- // Kick off the command.
- const execPromise = rawExec(cmd, {}, [], abortController.signal);
+ // Start a bash shell that:
+ // - spawns a background `sleep 30`
+ // - prints the PID of the `sleep`
+ // - waits for `sleep` to exit
+ const { stdout, exitCode } = await (async () => {
+ const p = rawExec(cmd, {}, [], abortController.signal);
- // Give Bash a tiny bit of time to start and print the PID.
- await new Promise((r) => setTimeout(r, 100));
+ // Give Bash a tiny bit of time to start and print the PID.
+ await new Promise((r) => setTimeout(r, 100));
- // Cancel the task – this should kill *both* bash and the inner sleep.
- abortController.abort();
+ // Cancel the task – this should kill *both* bash and the inner sleep.
+ abortController.abort();
- const { exitCode, stdout } = await execPromise;
+ // Wait for rawExec to resolve after aborting
+ return p;
+ })();
// We expect a non‑zero exit code because the process was killed.
expect(exitCode).not.toBe(0);
- // Attempt to extract the grand‑child PID from stdout.
- const pidMatch = /^(\d+)/.exec(stdout.trim());
-
- if (pidMatch) {
- const sleepPid = Number(pidMatch[1]);
-
- // Verify that the sleep process is no longer alive.
- let alive = true;
- try {
- process.kill(sleepPid, 0);
- } catch (error: any) {
- // Check if error is ESRCH (No such process)
- if (error.code === "ESRCH") {
- alive = false; // Process is dead, as expected.
- } else {
- throw error;
- }
- }
- expect(alive).toBe(false);
- } else {
- // If PID was not printed, it implies bash was killed very early.
- // The test passes implicitly in this scenario as the abort mechanism
- // successfully stopped the command execution quickly.
- expect(true).toBe(true);
+ // Extract the PID of the sleep process that bash printed
+ const pid = Number(stdout.trim().match(/^\d+/)?.[0]);
+ if (pid) {
+ // Confirm that the sleep process is no longer alive
+ await ensureProcessGone(pid);
}
});
});
+
+/**
+ * Waits until a process no longer exists, or throws after timeout.
+ * @param pid - The process ID to check
+ * @throws {Error} If the process is still alive after 500ms
+ */
+async function ensureProcessGone(pid: number) {
+ const timeout = 500;
+ const deadline = Date.now() + timeout;
+ while (Date.now() < deadline) {
+ try {
+ process.kill(pid, 0); // check if process still exists
+ await new Promise((r) => setTimeout(r, 50)); // wait and retry
+ } catch (e: any) {
+ if (e.code === "ESRCH") {
+ return; // process is gone — success
+ }
+ throw e; // unexpected error — rethrow
+ }
+ }
+ throw new Error(
+ `Process with PID ${pid} failed to terminate within ${timeout}ms`,
+ );
+}
diff --git a/codex-cli/tests/requires-shell.test.ts b/codex-cli/tests/requires-shell.test.ts
new file mode 100644
index 0000000000..5264490e72
--- /dev/null
+++ b/codex-cli/tests/requires-shell.test.ts
@@ -0,0 +1,49 @@
+import { describe, it, expect } from "vitest";
+import { parse } from "shell-quote";
+
+/* eslint-disable no-console */
+
+// Recreate the requiresShell function for testing
+function requiresShell(cmd: Array): boolean {
+ // If the command is a single string that contains shell operators,
+ // it needs to be run with shell: true
+ if (cmd.length === 1 && cmd[0] !== undefined) {
+ const tokens = parse(cmd[0]) as Array;
+ console.log(
+ `Parsing argument: "${cmd[0]}", tokens:`,
+ JSON.stringify(tokens, null, 2),
+ );
+ return tokens.some((token) => typeof token === "object" && "op" in token);
+ }
+
+ // If the command is split into multiple arguments, we don't need shell: true
+ // even if one of the arguments is a shell operator like '|'
+ cmd.forEach((arg) => {
+ const tokens = parse(arg) as Array;
+ console.log(
+ `Parsing argument: "${arg}", tokens:`,
+ JSON.stringify(tokens, null, 2),
+ );
+ });
+ console.log("Result for separate arguments: false");
+ return false;
+}
+
+describe("requiresShell function", () => {
+ it("should detect pipe in a single argument", () => {
+ const cmd = ['grep -n "finally:" some-file | head'];
+ expect(requiresShell(cmd)).toBe(true);
+ });
+
+ it("should not detect pipe in separate arguments", () => {
+ const cmd = ["grep", "-n", "finally:", "some-file", "|", "head"];
+ const result = requiresShell(cmd);
+ console.log("Result for separate arguments:", result);
+ expect(result).toBe(false);
+ });
+
+ it("should handle other shell operators", () => {
+ const cmd = ["echo hello && echo world"];
+ expect(requiresShell(cmd)).toBe(true);
+ });
+});
diff --git a/codex-cli/tests/responses-chat-completions.test.ts b/codex-cli/tests/responses-chat-completions.test.ts
new file mode 100644
index 0000000000..e48366f8f4
--- /dev/null
+++ b/codex-cli/tests/responses-chat-completions.test.ts
@@ -0,0 +1,815 @@
+import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
+import type { OpenAI } from "openai";
+import type {
+ ResponseCreateInput,
+ ResponseEvent,
+} from "../src/utils/responses";
+import type {
+ ResponseInputItem,
+ Tool,
+ ResponseCreateParams,
+ ResponseFunctionToolCallItem,
+ ResponseFunctionToolCall,
+} from "openai/resources/responses/responses";
+
+// Define specific types for streaming and non-streaming params
+type ResponseCreateParamsStreaming = ResponseCreateParams & { stream: true };
+type ResponseCreateParamsNonStreaming = ResponseCreateParams & {
+ stream?: false;
+};
+
+// Define additional type guard for tool calls done event
+type ToolCallsDoneEvent = Extract<
+ ResponseEvent,
+ { type: "response.function_call_arguments.done" }
+>;
+type OutputTextDeltaEvent = Extract<
+ ResponseEvent,
+ { type: "response.output_text.delta" }
+>;
+type OutputTextDoneEvent = Extract<
+ ResponseEvent,
+ { type: "response.output_text.done" }
+>;
+type ResponseCompletedEvent = Extract<
+ ResponseEvent,
+ { type: "response.completed" }
+>;
+
+// Mock state to control the OpenAI client behavior
+const openAiState: {
+ createSpy?: ReturnType;
+ createStreamSpy?: ReturnType;
+} = {};
+
+// Mock the OpenAI client
+vi.mock("openai", () => {
+ class FakeOpenAI {
+ public chat = {
+ completions: {
+ create: (...args: Array) => {
+ if (args[0]?.stream) {
+ return openAiState.createStreamSpy!(...args);
+ }
+ return openAiState.createSpy!(...args);
+ },
+ },
+ };
+ }
+
+ return {
+ __esModule: true,
+ default: FakeOpenAI,
+ };
+});
+
+// Helper function to create properly typed test inputs
+function createTestInput(options: {
+ model: string;
+ userMessage: string;
+ stream?: boolean;
+ tools?: Array;
+ previousResponseId?: string;
+}): ResponseCreateInput {
+ const message: ResponseInputItem.Message = {
+ type: "message",
+ role: "user",
+ content: [
+ {
+ type: "input_text" as const,
+ text: options.userMessage,
+ },
+ ],
+ };
+
+ const input: ResponseCreateInput = {
+ model: options.model,
+ input: [message],
+ };
+
+ if (options.stream !== undefined) {
+ // @ts-expect-error TypeScript doesn't recognize this is valid
+ input.stream = options.stream;
+ }
+
+ if (options.tools) {
+ input.tools = options.tools;
+ }
+
+ if (options.previousResponseId) {
+ input.previous_response_id = options.previousResponseId;
+ }
+
+ return input;
+}
+
+// Type guard for function call content
+function isFunctionCall(content: any): content is ResponseFunctionToolCall {
+ return (
+ content && typeof content === "object" && content.type === "function_call"
+ );
+}
+
+// Additional type guard for tool call
+function isToolCall(item: any): item is ResponseFunctionToolCallItem {
+ return item && typeof item === "object" && item.type === "function";
+}
+
+// Type guards for various event types
+export function _isToolCallsDoneEvent(
+ event: ResponseEvent,
+): event is ToolCallsDoneEvent {
+ return event.type === "response.function_call_arguments.done";
+}
+
+function isOutputTextDeltaEvent(
+ event: ResponseEvent,
+): event is OutputTextDeltaEvent {
+ return event.type === "response.output_text.delta";
+}
+
+function isOutputTextDoneEvent(
+ event: ResponseEvent,
+): event is OutputTextDoneEvent {
+ return event.type === "response.output_text.done";
+}
+
+function isResponseCompletedEvent(
+ event: ResponseEvent,
+): event is ResponseCompletedEvent {
+ return event.type === "response.completed";
+}
+
+// Helper function to create a mock stream for tool calls testing
+function createToolCallsStream() {
+ async function* fakeToolStream() {
+ yield {
+ id: "chatcmpl-123",
+ model: "gpt-4o",
+ choices: [
+ {
+ delta: { role: "assistant" },
+ finish_reason: null,
+ index: 0,
+ },
+ ],
+ };
+ yield {
+ id: "chatcmpl-123",
+ model: "gpt-4o",
+ choices: [
+ {
+ delta: {
+ tool_calls: [
+ {
+ index: 0,
+ id: "call_123",
+ type: "function",
+ function: { name: "get_weather" },
+ },
+ ],
+ },
+ finish_reason: null,
+ index: 0,
+ },
+ ],
+ };
+ yield {
+ id: "chatcmpl-123",
+ model: "gpt-4o",
+ choices: [
+ {
+ delta: {
+ tool_calls: [
+ {
+ index: 0,
+ function: {
+ arguments: '{"location":"San Franci',
+ },
+ },
+ ],
+ },
+ finish_reason: null,
+ index: 0,
+ },
+ ],
+ };
+ yield {
+ id: "chatcmpl-123",
+ model: "gpt-4o",
+ choices: [
+ {
+ delta: {
+ tool_calls: [
+ {
+ index: 0,
+ function: {
+ arguments: 'sco"}',
+ },
+ },
+ ],
+ },
+ finish_reason: null,
+ index: 0,
+ },
+ ],
+ };
+ yield {
+ id: "chatcmpl-123",
+ model: "gpt-4o",
+ choices: [
+ {
+ delta: {},
+ finish_reason: "tool_calls",
+ index: 0,
+ },
+ ],
+ usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
+ };
+ }
+
+ return fakeToolStream();
+}
+
+describe("responsesCreateViaChatCompletions", () => {
+ // Using any type here to avoid import issues
+ let responsesModule: any;
+
+ beforeEach(async () => {
+ vi.resetModules();
+ responsesModule = await import("../src/utils/responses");
+ });
+
+ afterEach(() => {
+ vi.resetAllMocks();
+ openAiState.createSpy = undefined;
+ openAiState.createStreamSpy = undefined;
+ });
+
+ describe("non-streaming mode", () => {
+ it("should convert basic user message to chat completions format", async () => {
+ // Setup mock response
+ openAiState.createSpy = vi.fn().mockResolvedValue({
+ id: "chat-123",
+ model: "gpt-4o",
+ choices: [
+ {
+ message: {
+ role: "assistant",
+ content: "This is a test response",
+ },
+ finish_reason: "stop",
+ },
+ ],
+ usage: {
+ prompt_tokens: 10,
+ completion_tokens: 5,
+ total_tokens: 15,
+ },
+ });
+
+ const openaiClient = new (await import("openai")).default({
+ apiKey: "test-key",
+ }) as unknown as OpenAI;
+
+ const inputMessage = createTestInput({
+ model: "gpt-4o",
+ userMessage: "Hello world",
+ stream: false,
+ });
+
+ const result = await responsesModule.responsesCreateViaChatCompletions(
+ openaiClient,
+ inputMessage as ResponseCreateParams & { stream?: false | undefined },
+ );
+
+ // Verify OpenAI was called with correct parameters
+ expect(openAiState.createSpy).toHaveBeenCalledTimes(1);
+
+ // Skip type checking for mock objects in tests - this is acceptable for test code
+ // @ts-ignore
+ const callArgs = openAiState.createSpy?.mock?.calls?.[0]?.[0];
+ if (callArgs) {
+ expect(callArgs.model).toBe("gpt-4o");
+ expect(callArgs.messages).toEqual([
+ { role: "user", content: "Hello world" },
+ ]);
+ expect(callArgs.stream).toBe(false);
+ }
+
+ // Verify result format
+ expect(result.id).toBeDefined();
+ expect(result.object).toBe("response");
+ expect(result.model).toBe("gpt-4o");
+ expect(result.status).toBe("completed");
+ expect(result.output).toHaveLength(1);
+
+ // Use type guard to check the output item type
+ const outputItem = result.output[0];
+ expect(outputItem).toBeDefined();
+
+ if (outputItem && outputItem.type === "message") {
+ expect(outputItem.role).toBe("assistant");
+ expect(outputItem.content).toHaveLength(1);
+
+ const content = outputItem.content[0];
+ if (content && content.type === "output_text") {
+ expect(content.text).toBe("This is a test response");
+ }
+ }
+
+ expect(result.usage?.total_tokens).toBe(15);
+ });
+
+ it("should handle function calling correctly", async () => {
+ // Setup mock response with tool calls
+ openAiState.createSpy = vi.fn().mockResolvedValue({
+ id: "chat-456",
+ model: "gpt-4o",
+ choices: [
+ {
+ message: {
+ role: "assistant",
+ content: null,
+ tool_calls: [
+ {
+ id: "call_abc123",
+ type: "function",
+ function: {
+ name: "get_weather",
+ arguments: JSON.stringify({ location: "New York" }),
+ },
+ },
+ ],
+ },
+ finish_reason: "tool_calls",
+ },
+ ],
+ usage: {
+ prompt_tokens: 15,
+ completion_tokens: 8,
+ total_tokens: 23,
+ },
+ });
+
+ const openaiClient = new (await import("openai")).default({
+ apiKey: "test-key",
+ }) as unknown as OpenAI;
+
+ // Define function tool correctly
+ const weatherTool = {
+ type: "function" as const,
+ name: "get_weather",
+ description: "Get the current weather",
+ strict: true,
+ parameters: {
+ type: "object",
+ properties: {
+ location: { type: "string" },
+ },
+ required: ["location"],
+ },
+ };
+
+ const inputMessage = createTestInput({
+ model: "gpt-4o",
+ userMessage: "What's the weather in New York?",
+ tools: [weatherTool as any],
+ stream: false,
+ });
+
+ const result = await responsesModule.responsesCreateViaChatCompletions(
+ openaiClient,
+ inputMessage as ResponseCreateParams & { stream: false },
+ );
+
+ // Verify OpenAI was called with correct parameters
+ expect(openAiState.createSpy).toHaveBeenCalledTimes(1);
+
+ // Skip type checking for mock objects in tests
+ // @ts-ignore
+ const callArgs = openAiState.createSpy?.mock?.calls?.[0]?.[0];
+ if (callArgs) {
+ expect(callArgs.model).toBe("gpt-4o");
+ expect(callArgs.tools).toHaveLength(1);
+ expect(callArgs.tools[0].function.name).toBe("get_weather");
+ }
+
+ // Verify function call output directly instead of trying to check type
+ expect(result.output).toHaveLength(1);
+
+ const outputItem = result.output[0];
+ if (outputItem && outputItem.type === "message") {
+ const content = outputItem.content[0];
+
+ // Use the type guard function
+ expect(isFunctionCall(content)).toBe(true);
+
+ // Using type assertion after type guard check
+ if (isFunctionCall(content)) {
+ // These properties should exist on ResponseFunctionToolCall
+ expect((content as any).name).toBe("get_weather");
+ expect(JSON.parse((content as any).arguments).location).toBe(
+ "New York",
+ );
+ }
+ }
+ });
+
+ it("should preserve conversation history", async () => {
+ // First interaction
+ openAiState.createSpy = vi.fn().mockResolvedValue({
+ id: "chat-789",
+ model: "gpt-4o",
+ choices: [
+ {
+ message: {
+ role: "assistant",
+ content: "Hello! How can I help you?",
+ },
+ finish_reason: "stop",
+ },
+ ],
+ usage: { prompt_tokens: 5, completion_tokens: 6, total_tokens: 11 },
+ });
+
+ const openaiClient = new (await import("openai")).default({
+ apiKey: "test-key",
+ }) as unknown as OpenAI;
+
+ const firstInput = createTestInput({
+ model: "gpt-4o",
+ userMessage: "Hi there",
+ stream: false,
+ });
+
+ const firstResponse =
+ await responsesModule.responsesCreateViaChatCompletions(
+ openaiClient,
+ firstInput as unknown as ResponseCreateParamsNonStreaming & {
+ stream?: false | undefined;
+ },
+ );
+
+ // Reset the mock for second interaction
+ openAiState.createSpy.mockReset();
+ openAiState.createSpy = vi.fn().mockResolvedValue({
+ id: "chat-790",
+ model: "gpt-4o",
+ choices: [
+ {
+ message: {
+ role: "assistant",
+ content: "I'm an AI assistant created by Anthropic.",
+ },
+ finish_reason: "stop",
+ },
+ ],
+ usage: { prompt_tokens: 15, completion_tokens: 10, total_tokens: 25 },
+ });
+
+ // Second interaction with previous_response_id
+ const secondInput = createTestInput({
+ model: "gpt-4o",
+ userMessage: "Who are you?",
+ previousResponseId: firstResponse.id,
+ stream: false,
+ });
+
+ await responsesModule.responsesCreateViaChatCompletions(
+ openaiClient,
+ secondInput as unknown as ResponseCreateParamsNonStreaming & {
+ stream?: false | undefined;
+ },
+ );
+
+ // Verify history was included in second call
+ expect(openAiState.createSpy).toHaveBeenCalledTimes(1);
+
+ // Skip type checking for mock objects in tests
+ // @ts-ignore
+ const secondCallArgs = openAiState.createSpy?.mock?.calls?.[0]?.[0];
+ if (secondCallArgs) {
+ // Should have 3 messages: original user, assistant response, and new user message
+ expect(secondCallArgs.messages).toHaveLength(3);
+ expect(secondCallArgs.messages[0].role).toBe("user");
+ expect(secondCallArgs.messages[0].content).toBe("Hi there");
+ expect(secondCallArgs.messages[1].role).toBe("assistant");
+ expect(secondCallArgs.messages[1].content).toBe(
+ "Hello! How can I help you?",
+ );
+ expect(secondCallArgs.messages[2].role).toBe("user");
+ expect(secondCallArgs.messages[2].content).toBe("Who are you?");
+ }
+ });
+
+ it("handles tools correctly", async () => {
+ const testFunction = {
+ type: "function" as const,
+ name: "get_weather",
+ description: "Get the weather",
+ strict: true,
+ parameters: {
+ type: "object",
+ properties: {
+ location: {
+ type: "string",
+ description: "The location to get the weather for",
+ },
+ },
+ required: ["location"],
+ },
+ };
+
+ // Mock response with a tool call
+ openAiState.createSpy = vi.fn().mockResolvedValue({
+ id: "chatcmpl-123",
+ created: Date.now(),
+ model: "gpt-4o",
+ object: "chat.completion",
+ choices: [
+ {
+ message: {
+ role: "assistant",
+ content: null,
+ tool_calls: [
+ {
+ id: "call_123",
+ type: "function",
+ function: {
+ name: "get_weather",
+ arguments: JSON.stringify({ location: "San Francisco" }),
+ },
+ },
+ ],
+ },
+ finish_reason: "tool_calls",
+ index: 0,
+ },
+ ],
+ });
+
+ const openaiClient = new (await import("openai")).default({
+ apiKey: "test-key",
+ }) as unknown as OpenAI;
+
+ const inputMessage = createTestInput({
+ model: "gpt-4o",
+ userMessage: "What's the weather in San Francisco?",
+ tools: [testFunction],
+ });
+
+ const result = await responsesModule.responsesCreateViaChatCompletions(
+ openaiClient,
+ inputMessage as unknown as ResponseCreateParamsNonStreaming,
+ );
+
+ expect(result.status).toBe("requires_action");
+
+ // Cast result to include required_action to address TypeScript issues
+ const resultWithAction = result as any;
+
+ // Add null checks for required_action
+ expect(resultWithAction.required_action).not.toBeNull();
+ expect(resultWithAction.required_action?.type).toBe(
+ "submit_tool_outputs",
+ );
+
+ // Safely access the tool calls with proper null checks
+ const toolCalls =
+ resultWithAction.required_action?.submit_tool_outputs?.tool_calls || [];
+ expect(toolCalls.length).toBe(1);
+
+ if (toolCalls.length > 0) {
+ const toolCall = toolCalls[0];
+ expect(toolCall.type).toBe("function");
+
+ if (isToolCall(toolCall)) {
+ // Access with type assertion after type guard
+ expect((toolCall as any).function.name).toBe("get_weather");
+ expect(JSON.parse((toolCall as any).function.arguments)).toEqual({
+ location: "San Francisco",
+ });
+ }
+ }
+
+ // Only check model, messages, and tools in exact match
+ expect(openAiState.createSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ model: "gpt-4o",
+ messages: [
+ {
+ role: "user",
+ content: "What's the weather in San Francisco?",
+ },
+ ],
+ tools: [
+ expect.objectContaining({
+ type: "function",
+ function: {
+ name: "get_weather",
+ description: "Get the weather",
+ parameters: {
+ type: "object",
+ properties: {
+ location: {
+ type: "string",
+ description: "The location to get the weather for",
+ },
+ },
+ required: ["location"],
+ },
+ },
+ }),
+ ],
+ }),
+ );
+ });
+ });
+
+ describe("streaming mode", () => {
+ it("should handle streaming responses correctly", async () => {
+ // Mock an async generator for streaming
+ async function* fakeStream() {
+ yield {
+ id: "chatcmpl-123",
+ model: "gpt-4o",
+ choices: [
+ {
+ delta: { role: "assistant" },
+ finish_reason: null,
+ index: 0,
+ },
+ ],
+ };
+ yield {
+ id: "chatcmpl-123",
+ model: "gpt-4o",
+ choices: [
+ {
+ delta: { content: "Hello" },
+ finish_reason: null,
+ index: 0,
+ },
+ ],
+ };
+ yield {
+ id: "chatcmpl-123",
+ model: "gpt-4o",
+ choices: [
+ {
+ delta: { content: " world" },
+ finish_reason: null,
+ index: 0,
+ },
+ ],
+ };
+ yield {
+ id: "chatcmpl-123",
+ model: "gpt-4o",
+ choices: [
+ {
+ delta: {},
+ finish_reason: "stop",
+ index: 0,
+ },
+ ],
+ usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
+ };
+ }
+
+ openAiState.createStreamSpy = vi.fn().mockResolvedValue(fakeStream());
+
+ const openaiClient = new (await import("openai")).default({
+ apiKey: "test-key",
+ }) as unknown as OpenAI;
+
+ const inputMessage = createTestInput({
+ model: "gpt-4o",
+ userMessage: "Say hello",
+ stream: true,
+ });
+
+ const streamGenerator =
+ await responsesModule.responsesCreateViaChatCompletions(
+ openaiClient,
+ inputMessage as unknown as ResponseCreateParamsStreaming & {
+ stream: true;
+ },
+ );
+
+ // Collect all events from the stream
+ const events: Array = [];
+ for await (const event of streamGenerator) {
+ events.push(event);
+ }
+
+ // Verify stream generation
+ expect(events.length).toBeGreaterThan(0);
+
+ // Check initial events
+ const firstEvent = events[0];
+ const secondEvent = events[1];
+ expect(firstEvent?.type).toBe("response.created");
+ expect(secondEvent?.type).toBe("response.in_progress");
+
+ // Find content delta events using proper type guard
+ const deltaEvents = events.filter(isOutputTextDeltaEvent);
+
+ // Should have two delta events for "Hello" and " world"
+ expect(deltaEvents).toHaveLength(2);
+ expect(deltaEvents[0]?.delta).toBe("Hello");
+ expect(deltaEvents[1]?.delta).toBe(" world");
+
+ // Check final completion event with type guard
+ const completionEvent = events.find(isResponseCompletedEvent);
+ expect(completionEvent).toBeDefined();
+ if (completionEvent) {
+ expect(completionEvent.response.status).toBe("completed");
+ }
+
+ // Text should be concatenated
+ const textDoneEvent = events.find(isOutputTextDoneEvent);
+ expect(textDoneEvent).toBeDefined();
+ if (textDoneEvent) {
+ expect(textDoneEvent.text).toBe("Hello world");
+ }
+ });
+
+ it("handles streaming with tool calls", async () => {
+ // Mock a streaming response with tool calls
+ const mockStream = createToolCallsStream();
+ openAiState.createStreamSpy = vi.fn().mockReturnValue(mockStream);
+
+ const openaiClient = new (await import("openai")).default({
+ apiKey: "test-key",
+ }) as unknown as OpenAI;
+
+ const testFunction = {
+ type: "function" as const,
+ name: "get_weather",
+ description: "Get the current weather",
+ strict: true,
+ parameters: {
+ type: "object",
+ properties: {
+ location: { type: "string" },
+ },
+ required: ["location"],
+ },
+ };
+
+ const inputMessage = createTestInput({
+ model: "gpt-4o",
+ userMessage: "What's the weather in San Francisco?",
+ tools: [testFunction],
+ stream: true,
+ });
+
+ const streamGenerator =
+ await responsesModule.responsesCreateViaChatCompletions(
+ openaiClient,
+ inputMessage as unknown as ResponseCreateParamsStreaming,
+ );
+
+ // Collect all events from the stream
+ const events: Array = [];
+ for await (const event of streamGenerator) {
+ events.push(event);
+ }
+
+ // Verify stream generation
+ expect(events.length).toBeGreaterThan(0);
+
+ // Look for function call related events of any type related to tool calls
+ const toolCallEvents = events.filter(
+ (event) =>
+ event.type.includes("function_call") ||
+ event.type.includes("tool") ||
+ (event.type === "response.output_item.added" &&
+ "item" in event &&
+ event.item?.type === "function_call"),
+ );
+
+ expect(toolCallEvents.length).toBeGreaterThan(0);
+
+ // Check if we have the completed event which should contain the final result
+ const completedEvent = events.find(isResponseCompletedEvent);
+ expect(completedEvent).toBeDefined();
+
+ if (completedEvent) {
+ // Get the function call from the output array
+ const functionCallItem = completedEvent.response.output.find(
+ (item) => item.type === "function_call",
+ );
+ expect(functionCallItem).toBeDefined();
+
+ if (functionCallItem && functionCallItem.type === "function_call") {
+ expect(functionCallItem.name).toBe("get_weather");
+ // The arguments is a JSON string, but we can check if it includes San Francisco
+ expect(functionCallItem.arguments).toContain("San Francisco");
+ }
+ }
+ });
+ });
+});
diff --git a/codex-cli/tests/slash-commands.test.ts b/codex-cli/tests/slash-commands.test.ts
new file mode 100644
index 0000000000..b10a484f3b
--- /dev/null
+++ b/codex-cli/tests/slash-commands.test.ts
@@ -0,0 +1,39 @@
+import { test, expect } from "vitest";
+import { SLASH_COMMANDS, type SlashCommand } from "../src/utils/slash-commands";
+
+test("SLASH_COMMANDS includes expected commands", () => {
+ const commands = SLASH_COMMANDS.map((c: SlashCommand) => c.command);
+ expect(commands).toContain("/clear");
+ expect(commands).toContain("/compact");
+ expect(commands).toContain("/history");
+ expect(commands).toContain("/help");
+ expect(commands).toContain("/model");
+ expect(commands).toContain("/approval");
+ expect(commands).toContain("/clearhistory");
+ expect(commands).toContain("/diff");
+});
+
+test("filters slash commands by prefix", () => {
+ const prefix = "/c";
+ const filtered = SLASH_COMMANDS.filter((c: SlashCommand) =>
+ c.command.startsWith(prefix),
+ );
+ const names = filtered.map((c: SlashCommand) => c.command);
+ expect(names).toEqual(
+ expect.arrayContaining(["/clear", "/clearhistory", "/compact"]),
+ );
+ expect(names).not.toEqual(
+ expect.arrayContaining(["/history", "/help", "/model", "/approval"]),
+ );
+
+ const emptyPrefixFiltered = SLASH_COMMANDS.filter((c: SlashCommand) =>
+ c.command.startsWith(""),
+ );
+ const emptyPrefixNames = emptyPrefixFiltered.map(
+ (c: SlashCommand) => c.command,
+ );
+ expect(emptyPrefixNames).toEqual(
+ expect.arrayContaining(SLASH_COMMANDS.map((c: SlashCommand) => c.command)),
+ );
+ expect(emptyPrefixNames).toHaveLength(SLASH_COMMANDS.length);
+});
diff --git a/codex-cli/tests/terminal-chat-completions.test.tsx b/codex-cli/tests/terminal-chat-completions.test.tsx
new file mode 100644
index 0000000000..b3b0b14520
--- /dev/null
+++ b/codex-cli/tests/terminal-chat-completions.test.tsx
@@ -0,0 +1,46 @@
+import React from "react";
+import { describe, it, expect } from "vitest";
+import type { ComponentProps } from "react";
+import { renderTui } from "./ui-test-helpers.js";
+import TerminalChatCompletions from "../src/components/chat/terminal-chat-completions.js";
+
+describe("TerminalChatCompletions", () => {
+ const baseProps: ComponentProps = {
+ completions: ["Option 1", "Option 2", "Option 3", "Option 4", "Option 5"],
+ displayLimit: 3,
+ selectedCompletion: 0,
+ };
+
+ it("renders visible completions within displayLimit", async () => {
+ const { lastFrameStripped } = renderTui(
+ ,
+ );
+ const frame = lastFrameStripped();
+ expect(frame).toContain("Option 1");
+ expect(frame).toContain("Option 2");
+ expect(frame).toContain("Option 3");
+ expect(frame).not.toContain("Option 4");
+ });
+
+ it("centers the selected completion in the visible list", async () => {
+ const { lastFrameStripped } = renderTui(
+ ,
+ );
+ const frame = lastFrameStripped();
+ expect(frame).toContain("Option 2");
+ expect(frame).toContain("Option 3");
+ expect(frame).toContain("Option 4");
+ expect(frame).not.toContain("Option 1");
+ });
+
+ it("adjusts when selectedCompletion is near the end", async () => {
+ const { lastFrameStripped } = renderTui(
+ ,
+ );
+ const frame = lastFrameStripped();
+ expect(frame).toContain("Option 3");
+ expect(frame).toContain("Option 4");
+ expect(frame).toContain("Option 5");
+ expect(frame).not.toContain("Option 2");
+ });
+});
diff --git a/codex-cli/tests/terminal-chat-input-compact.test.tsx b/codex-cli/tests/terminal-chat-input-compact.test.tsx
index 194a61cae2..2120aab49d 100644
--- a/codex-cli/tests/terminal-chat-input-compact.test.tsx
+++ b/codex-cli/tests/terminal-chat-input-compact.test.tsx
@@ -17,12 +17,14 @@ describe("TerminalChatInput compact command", () => {
setItems: () => {},
contextLeftPercent: 10,
openOverlay: () => {},
+ openDiffOverlay: () => {},
openModelOverlay: () => {},
openApprovalOverlay: () => {},
openHelpOverlay: () => {},
onCompact: () => {},
interruptAgent: () => {},
active: true,
+ thinkingSeconds: 0,
};
const { lastFrameStripped } = renderTui( );
const frame = lastFrameStripped();
diff --git a/codex-cli/tests/terminal-chat-input-multiline.test.tsx b/codex-cli/tests/terminal-chat-input-multiline.test.tsx
new file mode 100644
index 0000000000..b992cd08b5
--- /dev/null
+++ b/codex-cli/tests/terminal-chat-input-multiline.test.tsx
@@ -0,0 +1,157 @@
+import React from "react";
+import type { ComponentProps } from "react";
+import { renderTui } from "./ui-test-helpers.js";
+import TerminalChatInput from "../src/components/chat/terminal-chat-input.js";
+import { describe, it, expect, vi } from "vitest";
+
+// Helper that lets us type and then immediately flush ink's async timers
+async function type(
+ stdin: NodeJS.WritableStream,
+ text: string,
+ flush: () => Promise,
+) {
+ stdin.write(text);
+ await flush();
+}
+
+// Mock the createInputItem function to avoid filesystem operations
+vi.mock("../src/utils/input-utils.js", () => ({
+ createInputItem: vi.fn(async (text: string) => ({
+ role: "user",
+ type: "message",
+ content: [{ type: "input_text", text }],
+ })),
+}));
+
+describe("TerminalChatInput multiline functionality", () => {
+ it("renders the multiline editor component", async () => {
+ const props: ComponentProps = {
+ isNew: false,
+ loading: false,
+ submitInput: () => {},
+ confirmationPrompt: null,
+ explanation: undefined,
+ submitConfirmation: () => {},
+ setLastResponseId: () => {},
+ setItems: () => {},
+ contextLeftPercent: 50,
+ openOverlay: () => {},
+ openDiffOverlay: () => {},
+ openModelOverlay: () => {},
+ openApprovalOverlay: () => {},
+ openHelpOverlay: () => {},
+ onCompact: () => {},
+ interruptAgent: () => {},
+ active: true,
+ thinkingSeconds: 0,
+ };
+
+ const { lastFrameStripped } = renderTui( );
+ const frame = lastFrameStripped();
+
+ // Check that the help text mentions shift+enter for new line
+ expect(frame).toContain("shift+enter for new line");
+ });
+
+ it("allows multiline input with shift+enter", async () => {
+ const submitInput = vi.fn();
+
+ const props: ComponentProps = {
+ isNew: false,
+ loading: false,
+ submitInput,
+ confirmationPrompt: null,
+ explanation: undefined,
+ submitConfirmation: () => {},
+ setLastResponseId: () => {},
+ setItems: () => {},
+ contextLeftPercent: 50,
+ openOverlay: () => {},
+ openDiffOverlay: () => {},
+ openModelOverlay: () => {},
+ openApprovalOverlay: () => {},
+ openHelpOverlay: () => {},
+ onCompact: () => {},
+ interruptAgent: () => {},
+ active: true,
+ thinkingSeconds: 0,
+ };
+
+ const { stdin, lastFrameStripped, flush, cleanup } = renderTui(
+ ,
+ );
+
+ // Type some text
+ await type(stdin, "first line", flush);
+
+ // Send Shift+Enter (CSI-u format)
+ await type(stdin, "\u001B[13;2u", flush);
+
+ // Type more text
+ await type(stdin, "second line", flush);
+
+ // Check that both lines are visible in the editor
+ const frame = lastFrameStripped();
+ expect(frame).toContain("first line");
+ expect(frame).toContain("second line");
+
+ // Submit the multiline input with Enter
+ await type(stdin, "\r", flush);
+
+ // Check that submitInput was called with the multiline text
+ expect(submitInput).toHaveBeenCalledTimes(1);
+
+ cleanup();
+ });
+
+ it("allows multiline input with shift+enter (modifyOtherKeys=1 format)", async () => {
+ const submitInput = vi.fn();
+
+ const props: ComponentProps = {
+ isNew: false,
+ loading: false,
+ submitInput,
+ confirmationPrompt: null,
+ explanation: undefined,
+ submitConfirmation: () => {},
+ setLastResponseId: () => {},
+ setItems: () => {},
+ contextLeftPercent: 50,
+ openOverlay: () => {},
+ openDiffOverlay: () => {},
+ openModelOverlay: () => {},
+ openApprovalOverlay: () => {},
+ openHelpOverlay: () => {},
+ onCompact: () => {},
+ interruptAgent: () => {},
+ active: true,
+ thinkingSeconds: 0,
+ };
+
+ const { stdin, lastFrameStripped, flush, cleanup } = renderTui(
+ ,
+ );
+
+ // Type some text
+ await type(stdin, "first line", flush);
+
+ // Send Shift+Enter (modifyOtherKeys=1 format)
+ await type(stdin, "\u001B[27;2;13~", flush);
+
+ // Type more text
+ await type(stdin, "second line", flush);
+
+ // Check that both lines are visible in the editor
+ const frame = lastFrameStripped();
+ expect(frame).toContain("first line");
+ expect(frame).toContain("second line");
+
+ // Submit the multiline input with Enter
+ await type(stdin, "\r", flush);
+
+ // Check that submitInput was called with the multiline text
+ expect(submitInput).toHaveBeenCalledTimes(1);
+
+ cleanup();
+ });
+});
diff --git a/codex-cli/tests/text-buffer.test.ts b/codex-cli/tests/text-buffer.test.ts
index c3f33d0fa1..e5c532f7cb 100644
--- a/codex-cli/tests/text-buffer.test.ts
+++ b/codex-cli/tests/text-buffer.test.ts
@@ -127,7 +127,7 @@ describe("TextBuffer – basic editing parity with Rust suite", () => {
expect(buf.getCursor()).toEqual([0, 2]); // after 'b'
});
- it("is a no‑op at the very beginning of the buffer", () => {
+ it("is a no-op at the very beginning of the buffer", () => {
const buf = new TextBuffer("ab");
buf.backspace(); // caret starts at (0,0)
diff --git a/codex-cli/tests/typeahead-scroll.test.tsx b/codex-cli/tests/typeahead-scroll.test.tsx
index fab7c753b8..e0e496adac 100644
--- a/codex-cli/tests/typeahead-scroll.test.tsx
+++ b/codex-cli/tests/typeahead-scroll.test.tsx
@@ -26,7 +26,7 @@ vi.mock("../src/components/select-input/select-input.js", () => {
// Ink's toggles raw‑mode which calls .ref() / .unref() on stdin.
// The test environment's mock streams don't implement those methods, so we
-// polyfill them to no‑ops on the prototype *before* the component tree mounts.
+// polyfill them to no-ops on the prototype *before* the component tree mounts.
import { EventEmitter } from "node:events";
if (!(EventEmitter.prototype as any).ref) {
(EventEmitter.prototype as any).ref = () => {};
diff --git a/codex-cli/tsconfig.json b/codex-cli/tsconfig.json
index d1dacc9149..43a2287e9b 100644
--- a/codex-cli/tsconfig.json
+++ b/codex-cli/tsconfig.json
@@ -11,7 +11,7 @@
],
"types": ["node"],
"baseUrl": "./",
- "resolveJsonModule": false, // ESM doesn't yet support JSON modules.
+ "resolveJsonModule": true, // ESM doesn't yet support JSON modules.
"jsx": "react",
"declaration": true,
"newLine": "lf",
@@ -30,5 +30,5 @@
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
- "include": ["src", "tests"]
+ "include": ["src", "tests", "bin"]
}
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index 0e7616fa96..0000000000
--- a/package-lock.json
+++ /dev/null
@@ -1,359 +0,0 @@
-{
- "name": "codex",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "devDependencies": {
- "git-cliff": "^2.8.0",
- "prettier": "^3.5.3"
- }
- },
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/execa": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
- "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cross-spawn": "^7.0.3",
- "get-stream": "^8.0.1",
- "human-signals": "^5.0.0",
- "is-stream": "^3.0.0",
- "merge-stream": "^2.0.0",
- "npm-run-path": "^5.1.0",
- "onetime": "^6.0.0",
- "signal-exit": "^4.1.0",
- "strip-final-newline": "^3.0.0"
- },
- "engines": {
- "node": ">=16.17"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/execa?sponsor=1"
- }
- },
- "node_modules/get-stream": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
- "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/git-cliff": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/git-cliff/-/git-cliff-2.8.0.tgz",
- "integrity": "sha512-iKF5QTXAb9+iVvmu5HpnMPWYw7fs74xkpAaRbSf29+dZaMTTNRIUST/y+Ir2S1bDUWWJNjXlwT9ZT62JuYLQnA==",
- "dev": true,
- "license": "MIT OR Apache-2.0",
- "dependencies": {
- "execa": "^8.0.1"
- },
- "bin": {
- "git-cliff": "lib/cli/cli.js"
- },
- "engines": {
- "node": ">=18.19 || >=20.6 || >=21"
- },
- "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"
- }
- },
- "node_modules/git-cliff-darwin-arm64": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/git-cliff-darwin-arm64/-/git-cliff-darwin-arm64-2.8.0.tgz",
- "integrity": "sha512-rurUV2d1Z2n+c2+wUrO0gZaFb3c1G+ej0bPfKTPfde/CblxiysMkh+4dz23NrVbc8IlS5rSYv/JFGVaVSBNJRw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT OR Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/git-cliff-darwin-x64": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/git-cliff-darwin-x64/-/git-cliff-darwin-x64-2.8.0.tgz",
- "integrity": "sha512-Wtj+FGWZBWmeYUAGlkfz7QPz4+VVxxDPMhQ/7iwKVA3iryIX0slGfzYpqMurEFnTAMr0r+4IU3Q4O/ib7iUscg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT OR Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/git-cliff-linux-arm64": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/git-cliff-linux-arm64/-/git-cliff-linux-arm64-2.8.0.tgz",
- "integrity": "sha512-k4RdfMdORXyefznWlQb+7wDgo7XgQF9qg8hJC34bwyJK2sODirrGau3uTx1/9Fi37g+pAOM7wM+LYppHCTZ2bQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT OR Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/git-cliff-linux-x64": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/git-cliff-linux-x64/-/git-cliff-linux-x64-2.8.0.tgz",
- "integrity": "sha512-FcWX4GHgodYrQlZR03fzooanStgR03JNWvyaMQB1asplQ18nlziK2UyA+PESCIxOQmeLXauqoCApfzmdtp5myg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT OR Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/git-cliff-windows-arm64": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/git-cliff-windows-arm64/-/git-cliff-windows-arm64-2.8.0.tgz",
- "integrity": "sha512-GJSrqmBVTbMtBJI3/YCDxLviZZDgYgnKqYgquBk2u2AELAnnuWFnVFQ7ZEBUqgFF2UJu9EdV2Nv6MV8d/wnP0g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT OR Apache-2.0",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/git-cliff-windows-x64": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/git-cliff-windows-x64/-/git-cliff-windows-x64-2.8.0.tgz",
- "integrity": "sha512-8jl0YMXPYjUmVygUEeQ4wf1zte3Rv8LPq1sIklUKl80XE4g2Gm/8EIWbKpUPLQH6IncRwepY6VuMgpVpPXbwNw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT OR Apache-2.0",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/human-signals": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
- "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=16.17.0"
- }
- },
- "node_modules/is-stream": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
- "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/merge-stream": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
- "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/mimic-fn": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
- "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/npm-run-path": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
- "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/npm-run-path/node_modules/path-key": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
- "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/onetime": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
- "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mimic-fn": "^4.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/prettier": {
- "version": "3.5.3",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
- "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "prettier": "bin/prettier.cjs"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/prettier/prettier?sponsor=1"
- }
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/strip-final-newline": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
- "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- }
- }
-}
diff --git a/package.json b/package.json
index d967dc3435..b516ce40fa 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,46 @@
{
+ "name": "codex-monorepo",
+ "private": true,
"description": "Tools for repo-wide maintenance.",
"scripts": {
- "release": "cd codex-cli && npm run release",
+ "release": "pnpm --filter @openai/codex run release",
"format": "prettier --check *.json *.md .github/workflows/*.yml",
"format:fix": "prettier --write *.json *.md .github/workflows/*.yml",
- "changelog": "git-cliff --config cliff.toml --output CHANGELOG.ignore.md $LAST_RELEASE_TAG..HEAD"
+ "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"
},
"devDependencies": {
"git-cliff": "^2.8.0",
+ "husky": "^9.1.7",
+ "lint-staged": "^15.5.1",
"prettier": "^3.5.3"
- }
+ },
+ "resolutions": {
+ "braces": "^3.0.3",
+ "micromatch": "^4.0.8",
+ "semver": "^7.7.1"
+ },
+ "overrides": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=22",
+ "pnpm": ">=9.0.0"
+ },
+ "lint-staged": {
+ "*.json": "prettier --write",
+ "*.md": "prettier --write",
+ ".github/workflows/*.yml": "prettier --write",
+ "**/*.{js,ts,tsx}": [
+ "cd codex-cli && pnpm run lint",
+ "cd codex-cli && pnpm run typecheck"
+ ]
+ },
+ "packageManager": "pnpm@10.8.1"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000000..69491efb95
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,4963 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+overrides:
+ braces: ^3.0.3
+ micromatch: ^4.0.8
+ semver: ^7.7.1
+
+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
+ 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
+ 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(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/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: ^2.8.7
+ version: 2.8.8
+ 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
+ vitest:
+ specifier: ^3.0.9
+ version: 3.1.1(@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/cardinal@2.1.1':
+ resolution: {integrity: sha512-/xCVwg8lWvahHsV2wXZt4i64H1sdL+sN1Uoq7fAc8/FA6uYHjuIveDwPwvGUYp4VZiv85dVl6J/Bum3NDAOm8g==}
+
+ '@types/diff@7.0.2':
+ resolution: {integrity: sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==}
+
+ '@types/estree@1.0.7':
+ resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
+
+ '@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/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/react@18.3.20':
+ resolution: {integrity: sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==}
+
+ '@types/semver@7.7.0':
+ resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==}
+
+ '@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.1':
+ resolution: {integrity: sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==}
+
+ '@vitest/mocker@3.1.1':
+ resolution: {integrity: sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^5.0.0 || ^6.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@3.1.1':
+ resolution: {integrity: sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==}
+
+ '@vitest/runner@3.1.1':
+ resolution: {integrity: sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==}
+
+ '@vitest/snapshot@3.1.1':
+ resolution: {integrity: sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==}
+
+ '@vitest/spy@3.1.1':
+ resolution: {integrity: sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==}
+
+ '@vitest/utils@3.1.1':
+ resolution: {integrity: sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==}
+
+ abort-controller@3.0.0:
+ resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
+ engines: {node: '>=6.5'}
+
+ 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
+
+ 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==}
+
+ 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'}
+
+ 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==}
+
+ convert-to-spaces@2.0.1:
+ resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ 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'}
+
+ 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'}
+
+ 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==}
+
+ 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-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'}
+
+ 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'}
+
+ 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.3:
+ resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==}
+ 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'}
+
+ 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'}
+
+ 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==}
+
+ 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
+
+ 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'}
+
+ 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-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'}
+
+ meow@13.2.0:
+ resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
+ 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-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ 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==}
+
+ node-domexception@1.0.0:
+ resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
+ engines: {node: '>=10.5.0'}
+
+ 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'}
+
+ 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==}
+
+ 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-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@2.8.8:
+ resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+
+ 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==}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ react-is@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
+
+ 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-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'}
+
+ 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
+
+ set-function-length@1.2.2:
+ resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+ engines: {node: '>= 0.4'}
+
+ set-function-name@2.0.2:
+ resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
+ engines: {node: '>= 0.4'}
+
+ set-proto@1.0.0:
+ resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
+ engines: {node: '>= 0.4'}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ 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==}
+
+ 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.12:
+ resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==}
+ 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'}
+
+ 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'}
+
+ 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'}
+
+ 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==}
+
+ vite-node@3.1.1:
+ resolution: {integrity: sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+
+ vite@6.3.1:
+ resolution: {integrity: sha512-kkzzkqtMESYklo96HKKPE5KKLkC1amlsqt+RjFMlX2AvbRB/0wghap19NdBxxwGZ+h/C6DLCrcEphPIItlGrRQ==}
+ 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.1:
+ resolution: {integrity: sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==}
+ 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.1
+ '@vitest/ui': 3.1.1
+ 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/cardinal@2.1.1': {}
+
+ '@types/diff@7.0.2': {}
+
+ '@types/estree@1.0.7': {}
+
+ '@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/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/react@18.3.20':
+ dependencies:
+ '@types/prop-types': 15.7.14
+ csstype: 3.1.3
+
+ '@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(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.1':
+ dependencies:
+ '@vitest/spy': 3.1.1
+ '@vitest/utils': 3.1.1
+ chai: 5.2.0
+ tinyrainbow: 2.0.0
+
+ '@vitest/mocker@3.1.1(vite@6.3.1(@types/node@22.14.1)(yaml@2.7.1))':
+ dependencies:
+ '@vitest/spy': 3.1.1
+ estree-walker: 3.0.3
+ magic-string: 0.30.17
+ optionalDependencies:
+ vite: 6.3.1(@types/node@22.14.1)(yaml@2.7.1)
+
+ '@vitest/pretty-format@3.1.1':
+ dependencies:
+ tinyrainbow: 2.0.0
+
+ '@vitest/runner@3.1.1':
+ dependencies:
+ '@vitest/utils': 3.1.1
+ pathe: 2.0.3
+
+ '@vitest/snapshot@3.1.1':
+ dependencies:
+ '@vitest/pretty-format': 3.1.1
+ magic-string: 0.30.17
+ pathe: 2.0.3
+
+ '@vitest/spy@3.1.1':
+ dependencies:
+ tinyspy: 3.0.2
+
+ '@vitest/utils@3.1.1':
+ dependencies:
+ '@vitest/pretty-format': 3.1.1
+ loupe: 3.1.3
+ tinyrainbow: 2.0.0
+
+ abort-controller@3.0.0:
+ dependencies:
+ event-target-shim: 5.0.1
+
+ 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: {}
+
+ 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: {}
+
+ 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
+
+ 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: {}
+
+ convert-to-spaces@2.0.1: {}
+
+ 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: {}
+
+ 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
+
+ emoji-regex@10.4.0: {}
+
+ emoji-regex@8.0.0: {}
+
+ emojilib@2.4.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-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: {}
+
+ 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: {}
+
+ 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.3(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
+
+ 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
+
+ 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: {}
+
+ human-signals@5.0.0: {}
+
+ humanize-ms@1.2.1:
+ dependencies:
+ ms: 2.1.3
+
+ husky@9.1.7: {}
+
+ 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
+
+ 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-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(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: {}
+
+ meow@13.2.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-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.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: {}
+
+ 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
+
+ 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: {}
+
+ 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-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@2.8.8: {}
+
+ 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
+
+ punycode@2.3.1: {}
+
+ queue-microtask@1.2.3: {}
+
+ 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
+
+ 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-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
+
+ scheduler@0.23.2:
+ dependencies:
+ loose-envify: 1.4.0
+
+ semver@7.7.1: {}
+
+ set-function-length@1.2.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+
+ set-function-name@2.0.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ functions-have-names: 1.2.3
+ has-property-descriptors: 1.0.2
+
+ set-proto@1.0.0:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ 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: {}
+
+ 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.12:
+ dependencies:
+ fdir: 6.4.3(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: {}
+
+ 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: {}
+
+ 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: {}
+
+ 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: {}
+
+ vite-node@3.1.1(@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.1(@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.1(@types/node@22.14.1)(yaml@2.7.1):
+ dependencies:
+ esbuild: 0.25.2
+ fdir: 6.4.3(picomatch@4.0.2)
+ picomatch: 4.0.2
+ postcss: 8.5.3
+ rollup: 4.40.0
+ tinyglobby: 0.2.12
+ optionalDependencies:
+ '@types/node': 22.14.1
+ fsevents: 2.3.3
+ yaml: 2.7.1
+
+ vitest@3.1.1(@types/node@22.14.1)(yaml@2.7.1):
+ dependencies:
+ '@vitest/expect': 3.1.1
+ '@vitest/mocker': 3.1.1(vite@6.3.1(@types/node@22.14.1)(yaml@2.7.1))
+ '@vitest/pretty-format': 3.1.1
+ '@vitest/runner': 3.1.1
+ '@vitest/snapshot': 3.1.1
+ '@vitest/spy': 3.1.1
+ '@vitest/utils': 3.1.1
+ 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
+ tinypool: 1.0.2
+ tinyrainbow: 2.0.0
+ vite: 6.3.1(@types/node@22.14.1)(yaml@2.7.1)
+ vite-node: 3.1.1(@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
new file mode 100644
index 0000000000..d3ac856082
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,7 @@
+packages:
+ - codex-cli
+ - docs
+ - packages/*
+
+ignoredBuiltDependencies:
+ - esbuild
diff --git a/scripts/asciicheck.py b/scripts/asciicheck.py
new file mode 100755
index 0000000000..812d1c6b69
--- /dev/null
+++ b/scripts/asciicheck.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+
+import argparse
+import sys
+from pathlib import Path
+
+"""
+Utility script that takes a list of files and returns non-zero if any of them
+contain non-ASCII characters other than those in the allowed list.
+
+If --fix is used, it will attempt to replace non-ASCII characters with ASCII
+equivalents.
+
+The motivation behind this script is that characters like U+00A0 (non-breaking
+space) can cause regexes not to match and can result in surprising anchor
+values for headings when GitHub renders Markdown as HTML.
+"""
+
+
+"""
+When --fix is used, perform the following substitutions.
+"""
+substitutions: dict[int, str] = {
+ 0x00A0: " ", # non-breaking space
+ 0x2011: "-", # non-breaking hyphen
+ 0x2013: "-", # en dash
+ 0x2014: "-", # em dash
+ 0x2018: "'", # left single quote
+ 0x2019: "'", # right single quote
+ 0x201C: '"', # left double quote
+ 0x201D: '"', # right double quote
+ 0x2026: "...", # ellipsis
+ 0x202F: " ", # narrow non-breaking space
+}
+
+"""
+Unicode codepoints that are allowed in addition to ASCII.
+Be conservative with this list.
+
+Note that it is always an option to use the hex HTML representation
+instead of the character itself so the source code is ASCII-only.
+For example, U+2728 (sparkles) can be written as `✨`.
+"""
+allowed_unicode_codepoints = {
+ 0x2728, # sparkles
+}
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Check for non-ASCII characters in files."
+ )
+ parser.add_argument(
+ "--fix",
+ action="store_true",
+ help="Rewrite files, replacing non-ASCII characters with ASCII equivalents, where possible.",
+ )
+ parser.add_argument(
+ "files",
+ nargs="+",
+ help="Files to check for non-ASCII characters.",
+ )
+ args = parser.parse_args()
+
+ has_errors = False
+ for filename in args.files:
+ path = Path(filename)
+ has_errors |= lint_utf8_ascii(path, fix=args.fix)
+ return 1 if has_errors else 0
+
+
+def lint_utf8_ascii(filename: Path, fix: bool) -> bool:
+ """Returns True if an error was printed."""
+ try:
+ with open(filename, "rb") as f:
+ raw = f.read()
+ text = raw.decode("utf-8")
+ except UnicodeDecodeError as e:
+ print("UTF-8 decoding error:")
+ print(f" byte offset: {e.start}")
+ print(f" reason: {e.reason}")
+ # Attempt to find line/column
+ partial = raw[: e.start]
+ line = partial.count(b"\n") + 1
+ col = e.start - (partial.rfind(b"\n") if b"\n" in partial else -1)
+ print(f" location: line {line}, column {col}")
+ return True
+
+ errors = []
+ for lineno, line in enumerate(text.splitlines(keepends=True), 1):
+ for colno, char in enumerate(line, 1):
+ codepoint = ord(char)
+ if char == "\n":
+ continue
+ if (
+ not (0x20 <= codepoint <= 0x7E)
+ and codepoint not in allowed_unicode_codepoints
+ ):
+ errors.append((lineno, colno, char, codepoint))
+
+ if errors:
+ for lineno, colno, char, codepoint in errors:
+ safe_char = repr(char)[1:-1] # nicely escape things like \u202f
+ print(
+ f"Invalid character at line {lineno}, column {colno}: U+{codepoint:04X} ({safe_char})"
+ )
+
+ if errors and fix:
+ print(f"Attempting to fix {filename}...")
+ num_replacements = 0
+ new_contents = ""
+ for char in text:
+ codepoint = ord(char)
+ if codepoint in substitutions:
+ num_replacements += 1
+ new_contents += substitutions[codepoint]
+ else:
+ new_contents += char
+ with open(filename, "w", encoding="utf-8") as f:
+ f.write(new_contents)
+ print(f"Fixed {num_replacements} of {len(errors)} errors in {filename}.")
+
+ return bool(errors)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/scripts/readme_toc.py b/scripts/readme_toc.py
new file mode 100755
index 0000000000..fb1ac066a7
--- /dev/null
+++ b/scripts/readme_toc.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+
+"""
+Utility script to verify (and optionally fix) the Table of Contents in a
+Markdown file. By default, it checks that the ToC between ``
+and `` matches the headings in the file. With --fix, it
+rewrites the file to update the ToC.
+"""
+
+import argparse
+import sys
+import re
+import difflib
+from pathlib import Path
+from typing import List
+
+# Markers for the Table of Contents section
+BEGIN_TOC: str = ""
+END_TOC: str = ""
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Check and optionally fix the README.md Table of Contents."
+ )
+ parser.add_argument(
+ "file", nargs="?", default="README.md", help="Markdown file to process"
+ )
+ parser.add_argument(
+ "--fix", action="store_true", help="Rewrite file with updated ToC"
+ )
+ args = parser.parse_args()
+ path = Path(args.file)
+ return check_or_fix(path, args.fix)
+
+
+def generate_toc_lines(content: str) -> List[str]:
+ """
+ Generate markdown list lines for headings (## to ######) in content.
+ """
+ lines = content.splitlines()
+ headings = []
+ in_code = False
+ for line in lines:
+ if line.strip().startswith("```"):
+ in_code = not in_code
+ continue
+ if in_code:
+ continue
+ m = re.match(r"^(#{2,6})\s+(.*)$", line)
+ if not m:
+ continue
+ level = len(m.group(1))
+ text = m.group(2).strip()
+ headings.append((level, text))
+
+ toc = []
+ for level, text in headings:
+ indent = " " * (level - 2)
+ slug = text.lower()
+ # normalize spaces and dashes
+ slug = slug.replace("\u00a0", " ")
+ slug = slug.replace("\u2011", "-").replace("\u2013", "-").replace("\u2014", "-")
+ # drop other punctuation
+ slug = re.sub(r"[^0-9a-z\s-]", "", slug)
+ slug = slug.strip().replace(" ", "-")
+ toc.append(f"{indent}- [{text}](#{slug})")
+ return toc
+
+
+def check_or_fix(readme_path: Path, fix: bool) -> int:
+ if not readme_path.is_file():
+ print(f"Error: file not found: {readme_path}", file=sys.stderr)
+ return 1
+ content = readme_path.read_text(encoding="utf-8")
+ lines = content.splitlines()
+ # locate ToC markers
+ try:
+ begin_idx = next(i for i, l in enumerate(lines) if l.strip() == BEGIN_TOC)
+ end_idx = next(i for i, l in enumerate(lines) if l.strip() == END_TOC)
+ except StopIteration:
+ print(
+ f"Error: Could not locate '{BEGIN_TOC}' or '{END_TOC}' in {readme_path}.",
+ file=sys.stderr,
+ )
+ return 1
+ # extract current ToC list items
+ current_block = lines[begin_idx + 1 : end_idx]
+ current = [l for l in current_block if l.lstrip().startswith("- [")]
+ # generate expected ToC
+ expected = generate_toc_lines(content)
+ if current == expected:
+ return 0
+ if not fix:
+ print(
+ "ERROR: README ToC is out of date. Diff between existing and generated ToC:"
+ )
+ # Show full unified diff of current vs expected
+ diff = difflib.unified_diff(
+ current,
+ expected,
+ fromfile="existing ToC",
+ tofile="generated ToC",
+ lineterm="",
+ )
+ for line in diff:
+ print(line)
+ return 1
+ # rebuild file with updated ToC
+ prefix = lines[: begin_idx + 1]
+ suffix = lines[end_idx:]
+ new_lines = prefix + [""] + expected + [""] + suffix
+ readme_path.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
+ print(f"Updated ToC in {readme_path}.")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())