├── .github ├── demo.gif └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.toml ├── LICENSE ├── NOTICE ├── README.md ├── codex-cli ├── .dockerignore ├── .editorconfig ├── .eslintrc.cjs ├── Dockerfile ├── build.mjs ├── examples │ ├── README.md │ ├── build-codex-demo │ │ ├── run.sh │ │ ├── runs │ │ │ └── .gitkeep │ │ └── task.yaml │ ├── camerascii │ │ ├── run.sh │ │ ├── runs │ │ │ └── .gitkeep │ │ ├── task.yaml │ │ └── template │ │ │ └── screenshot_details.md │ ├── impossible-pong │ │ ├── run.sh │ │ ├── runs │ │ │ └── .gitkeep │ │ ├── task.yaml │ │ └── template │ │ │ └── index.html │ ├── prompt-analyzer │ │ ├── run.sh │ │ ├── runs │ │ │ └── .gitkeep │ │ ├── task.yaml │ │ └── template │ │ │ ├── Clustering.ipynb │ │ │ ├── README.md │ │ │ ├── analysis.md │ │ │ ├── analysis_dbscan.md │ │ │ ├── cluster_prompts.py │ │ │ ├── plots │ │ │ ├── cluster_sizes.png │ │ │ └── tsne.png │ │ │ ├── plots_dbscan │ │ │ ├── cluster_sizes.png │ │ │ └── tsne.png │ │ │ └── prompts.csv │ └── prompting_guide.md ├── ignore-react-devtools-plugin.js ├── package-lock.json ├── package.json ├── require-shim.js ├── scripts │ ├── build_container.sh │ ├── init_firewall.sh │ └── run_in_container.sh ├── src │ ├── app.tsx │ ├── approvals.ts │ ├── cli-singlepass.tsx │ ├── cli.tsx │ ├── components │ │ ├── approval-mode-overlay.tsx │ │ ├── chat │ │ │ ├── message-history.tsx │ │ │ ├── multiline-editor.tsx │ │ │ ├── terminal-chat-command-review.tsx │ │ │ ├── terminal-chat-input-thinking.tsx │ │ │ ├── terminal-chat-input.tsx │ │ │ ├── terminal-chat-new-input.tsx │ │ │ ├── terminal-chat-past-rollout.tsx │ │ │ ├── terminal-chat-response-item.tsx │ │ │ ├── terminal-chat-tool-call-item.tsx │ │ │ ├── terminal-chat-utils.ts │ │ │ ├── terminal-chat.tsx │ │ │ ├── terminal-header.tsx │ │ │ ├── terminal-message-history.tsx │ │ │ └── use-message-grouping.ts │ │ ├── help-overlay.tsx │ │ ├── history-overlay.tsx │ │ ├── model-overlay.tsx │ │ ├── onboarding │ │ │ └── onboarding-approval-mode.tsx │ │ ├── select-input │ │ │ ├── indicator.tsx │ │ │ ├── item.tsx │ │ │ └── select-input.tsx │ │ ├── singlepass-cli-app.tsx │ │ ├── typeahead-overlay.tsx │ │ └── vendor │ │ │ ├── cli-spinners │ │ │ └── index.js │ │ │ ├── ink-select │ │ │ ├── index.js │ │ │ ├── option-map.js │ │ │ ├── select-option.js │ │ │ ├── select.js │ │ │ ├── theme.js │ │ │ ├── use-select-state.js │ │ │ └── use-select.js │ │ │ ├── ink-spinner.tsx │ │ │ └── ink-text-input.tsx │ ├── format-command.ts │ ├── hooks │ │ ├── use-confirmation.ts │ │ └── use-terminal-size.ts │ ├── parse-apply-patch.ts │ ├── text-buffer.ts │ ├── typings.d.ts │ └── utils │ │ ├── agent │ │ ├── agent-loop.ts │ │ ├── apply-patch.ts │ │ ├── exec.ts │ │ ├── handle-exec-command.ts │ │ ├── log.ts │ │ ├── parse-apply-patch.ts │ │ ├── platform-commands.ts │ │ ├── review.ts │ │ └── sandbox │ │ │ ├── interface.ts │ │ │ ├── macos-seatbelt.ts │ │ │ └── raw-exec.ts │ │ ├── approximate-tokens-used.ts │ │ ├── auto-approval-mode.js │ │ ├── auto-approval-mode.ts │ │ ├── check-in-git.ts │ │ ├── check-updates.ts │ │ ├── config.ts │ │ ├── input-utils.ts │ │ ├── model-utils.ts │ │ ├── parsers.ts │ │ ├── session.ts │ │ ├── short-path.ts │ │ ├── singlepass │ │ ├── code_diff.ts │ │ ├── context.ts │ │ ├── context_files.ts │ │ ├── context_limit.ts │ │ └── file_ops.ts │ │ ├── storage │ │ └── save-rollout.ts │ │ └── terminal.ts ├── tests │ ├── __fixtures__ │ │ ├── a.txt │ │ └── b.txt │ ├── __snapshots__ │ │ └── check-updates.test.ts.snap │ ├── agent-cancel-early.test.ts │ ├── agent-cancel-prev-response.test.ts │ ├── agent-cancel-race.test.ts │ ├── agent-cancel.test.ts │ ├── agent-function-call-id.test.ts │ ├── agent-generic-network-error.test.ts │ ├── agent-invalid-request-error.test.ts │ ├── agent-max-tokens-error.test.ts │ ├── agent-network-errors.test.ts │ ├── agent-project-doc.test.ts │ ├── agent-rate-limit-error.test.ts │ ├── agent-server-retry.test.ts │ ├── agent-terminate.test.ts │ ├── agent-thinking-time.test.ts │ ├── api-key.test.ts │ ├── apply-patch.test.ts │ ├── approvals.test.ts │ ├── cancel-exec.test.ts │ ├── check-updates.test.ts │ ├── config.test.tsx │ ├── dummy.test.ts │ ├── external-editor.test.ts │ ├── fixed-requires-shell.test.ts │ ├── format-command.test.ts │ ├── input-utils.test.ts │ ├── invalid-command-handling.test.ts │ ├── markdown.test.tsx │ ├── model-utils-network-error.test.ts │ ├── multiline-ctrl-enter-submit.test.tsx │ ├── multiline-dynamic-width.test.tsx │ ├── multiline-enter-submit-cr.test.tsx │ ├── multiline-external-editor-shortcut.test.tsx │ ├── multiline-history-behavior.test.tsx │ ├── multiline-input-test.ts │ ├── multiline-newline.test.tsx │ ├── multiline-shift-enter-crlf.test.tsx │ ├── multiline-shift-enter-mod1.test.tsx │ ├── multiline-shift-enter.test.tsx │ ├── parse-apply-patch.test.ts │ ├── pipe-command.test.ts │ ├── project-doc.test.ts │ ├── raw-exec-process-group.test.ts │ ├── requires-shell.test.ts │ ├── terminal-chat-response-item.test.tsx │ ├── text-buffer-copy-paste.test.ts │ ├── text-buffer-crlf.test.ts │ ├── text-buffer-gaps.test.ts │ ├── text-buffer-word.test.ts │ ├── text-buffer.test.ts │ ├── typeahead-scroll.test.tsx │ └── ui-test-helpers.tsx ├── tsconfig.json └── vite.config.ts ├── docs └── CLA.md ├── package-lock.json ├── package.json └── scripts └── interactive-cherry-pick-upstream.sh /.github/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymichael/open-codex/88bb03b3e5fcfcb6a39fbb76f510486cea5cddd9/.github/demo.gif -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: { branches: [main] } 5 | push: { branches: [main] } 6 | 7 | jobs: 8 | build-test: 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 10 11 | env: 12 | NODE_OPTIONS: --max-old-space-size=4096 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 22 21 | 22 | # Run codex-cli/ tasks first because they are higher signal. 23 | 24 | - name: Install dependencies (codex-cli) 25 | working-directory: codex-cli 26 | run: npm ci 27 | 28 | - name: Check formatting (codex-cli) 29 | working-directory: codex-cli 30 | run: npm run format 31 | 32 | - name: Run tests (codex-cli) 33 | working-directory: codex-cli 34 | run: npm run test 35 | 36 | - name: Lint (codex-cli) 37 | working-directory: codex-cli 38 | run: | 39 | npm run lint -- \ 40 | --rule "no-console:error" \ 41 | --rule "no-debugger:error" \ 42 | --max-warnings=-1 43 | 44 | - name: Type‑check (codex-cli) 45 | working-directory: codex-cli 46 | run: npm run typecheck 47 | 48 | - name: Build (codex-cli) 49 | working-directory: codex-cli 50 | run: npm run build 51 | 52 | # Run formatting checks in the root directory last. 53 | 54 | - name: Install dependencies (root) 55 | run: npm ci 56 | 57 | - name: Check formatting (root) 58 | run: npm run format 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | node_modules 3 | 4 | # build 5 | dist/ 6 | build/ 7 | out/ 8 | storybook-static/ 9 | 10 | # ignore README for publishing 11 | codex-cli/README.md 12 | 13 | # editor 14 | .vscode/ 15 | .idea/ 16 | .history/ 17 | *.swp 18 | *~ 19 | 20 | # cli tools 21 | CLAUDE.md 22 | .claude/ 23 | 24 | # caches 25 | .cache/ 26 | .turbo/ 27 | .parcel-cache/ 28 | .eslintcache 29 | .nyc_output/ 30 | .jest/ 31 | *.tsbuildinfo 32 | 33 | # logs 34 | *.log 35 | npm-debug.log* 36 | yarn-debug.log* 37 | yarn-error.log* 38 | 39 | # env 40 | .env* 41 | !.env.example 42 | 43 | # package 44 | *.tgz 45 | 46 | # ci 47 | .vercel/ 48 | .netlify/ 49 | 50 | # patches 51 | apply_patch/ 52 | 53 | # coverage 54 | coverage/ 55 | 56 | # os 57 | .DS_Store 58 | Thumbs.db 59 | Icon? 60 | .Spotlight-V100/ 61 | 62 | # Unwanted package managers 63 | .yarn/ 64 | yarn.lock 65 | pnpm-lock.yaml 66 | 67 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /codex-cli/dist 2 | /codex-cli/node_modules 3 | -------------------------------------------------------------------------------- /.prettierrc.toml: -------------------------------------------------------------------------------- 1 | printWidth = 80 2 | quoteProps = "consistent" 3 | semi = true 4 | tabWidth = 2 5 | trailingComma = "all" 6 | 7 | # Preserve existing behavior for markdown/text wrapping. 8 | proseWrap = "preserve" 9 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | OpenAI Codex 2 | Copyright 2025 OpenAI 3 | -------------------------------------------------------------------------------- /codex-cli/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /codex-cli/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | 7 | [*.{js,ts,jsx,tsx}] 8 | indent_style = space 9 | indent_size = 2 -------------------------------------------------------------------------------- /codex-cli/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: [ 10 | ".eslintrc.cjs", 11 | "build.mjs", 12 | "dist", 13 | "vite.config.ts", 14 | "src/components/vendor", 15 | ], 16 | parser: "@typescript-eslint/parser", 17 | parserOptions: { 18 | tsconfigRootDir: __dirname, 19 | project: ["./tsconfig.json"], 20 | }, 21 | plugins: ["import", "react-hooks", "react-refresh"], 22 | rules: { 23 | // Imports 24 | "@typescript-eslint/consistent-type-imports": "error", 25 | "import/no-cycle": ["error", { maxDepth: 1 }], 26 | "import/no-duplicates": "error", 27 | "import/order": [ 28 | "error", 29 | { 30 | groups: ["type"], 31 | "newlines-between": "always", 32 | alphabetize: { 33 | order: "asc", 34 | caseInsensitive: false, 35 | }, 36 | }, 37 | ], 38 | // We use the import/ plugin instead. 39 | "sort-imports": "off", 40 | 41 | "@typescript-eslint/array-type": ["error", { default: "generic" }], 42 | // FIXME(mbolin): Introduce this. 43 | // "@typescript-eslint/explicit-function-return-type": "error", 44 | "@typescript-eslint/explicit-module-boundary-types": "error", 45 | "@typescript-eslint/no-explicit-any": "error", 46 | "@typescript-eslint/switch-exhaustiveness-check": [ 47 | "error", 48 | { 49 | allowDefaultCaseForExhaustiveSwitch: false, 50 | requireDefaultForNonUnion: true, 51 | }, 52 | ], 53 | 54 | // Use typescript-eslint/no-unused-vars, no-unused-vars reports 55 | // false positives with typescript 56 | "no-unused-vars": "off", 57 | "@typescript-eslint/no-unused-vars": [ 58 | "error", 59 | { 60 | argsIgnorePattern: "^_", 61 | varsIgnorePattern: "^_", 62 | caughtErrorsIgnorePattern: "^_", 63 | }, 64 | ], 65 | 66 | curly: "error", 67 | 68 | eqeqeq: ["error", "always", { null: "never" }], 69 | "react-refresh/only-export-components": [ 70 | "error", 71 | { allowConstantExport: true }, 72 | ], 73 | "no-await-in-loop": "error", 74 | "no-bitwise": "error", 75 | "no-caller": "error", 76 | // This is fine during development, but should not be checked in. 77 | "no-console": "error", 78 | // This is fine during development, but should not be checked in. 79 | "no-debugger": "error", 80 | "no-duplicate-case": "error", 81 | "no-eval": "error", 82 | "no-ex-assign": "error", 83 | "no-return-await": "error", 84 | "no-param-reassign": "error", 85 | "no-script-url": "error", 86 | "no-self-compare": "error", 87 | "no-unsafe-finally": "error", 88 | "no-var": "error", 89 | "react-hooks/rules-of-hooks": "error", 90 | "react-hooks/exhaustive-deps": "error", 91 | }, 92 | overrides: [ 93 | { 94 | // apply only to files under tests/ 95 | files: ["tests/**/*.{ts,tsx,js,jsx}"], 96 | rules: { 97 | "@typescript-eslint/no-explicit-any": "off", 98 | "import/order": "off", 99 | "@typescript-eslint/explicit-module-boundary-types": "off", 100 | "@typescript-eslint/ban-ts-comment": "off", 101 | "@typescript-eslint/no-var-requires": "off", 102 | "no-await-in-loop": "off", 103 | "no-control-regex": "off", 104 | }, 105 | }, 106 | ], 107 | }; 108 | -------------------------------------------------------------------------------- /codex-cli/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-slim 2 | 3 | ARG TZ 4 | ENV TZ="$TZ" 5 | 6 | # Install basic development tools, ca-certificates, and iptables/ipset, then clean up apt cache to reduce image size 7 | RUN apt-get update && apt-get install -y --no-install-recommends \ 8 | aggregate \ 9 | ca-certificates \ 10 | curl \ 11 | dnsutils \ 12 | fzf \ 13 | gh \ 14 | git \ 15 | gnupg2 \ 16 | iproute2 \ 17 | ipset \ 18 | iptables \ 19 | jq \ 20 | less \ 21 | man-db \ 22 | procps \ 23 | sudo \ 24 | unzip \ 25 | ripgrep \ 26 | zsh \ 27 | && rm -rf /var/lib/apt/lists/* 28 | 29 | # Ensure default node user has access to /usr/local/share 30 | RUN mkdir -p /usr/local/share/npm-global && \ 31 | chown -R node:node /usr/local/share 32 | 33 | ARG USERNAME=node 34 | 35 | # Set up non-root user 36 | USER node 37 | 38 | # Install global packages 39 | ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global 40 | ENV PATH=$PATH:/usr/local/share/npm-global/bin 41 | 42 | # Install codex 43 | COPY dist/codex.tgz codex.tgz 44 | RUN npm install -g codex.tgz \ 45 | && npm cache clean --force \ 46 | && rm -rf /usr/local/share/npm-global/lib/node_modules/codex-cli/node_modules/.cache \ 47 | && rm -rf /usr/local/share/npm-global/lib/node_modules/codex-cli/tests \ 48 | && rm -rf /usr/local/share/npm-global/lib/node_modules/codex-cli/docs 49 | 50 | # Copy and set up firewall script 51 | COPY scripts/init_firewall.sh /usr/local/bin/ 52 | USER root 53 | RUN chmod +x /usr/local/bin/init_firewall.sh && \ 54 | echo "node ALL=(root) NOPASSWD: /usr/local/bin/init_firewall.sh" > /etc/sudoers.d/node-firewall && \ 55 | chmod 0440 /etc/sudoers.d/node-firewall 56 | USER node 57 | -------------------------------------------------------------------------------- /codex-cli/build.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from "esbuild"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | 5 | const OUT_DIR = 'dist' 6 | /** 7 | * ink attempts to import react-devtools-core in an ESM-unfriendly way: 8 | * 9 | * https://github.com/vadimdemedes/ink/blob/eab6ef07d4030606530d58d3d7be8079b4fb93bb/src/reconciler.ts#L22-L45 10 | * 11 | * to make this work, we have to strip the import out of the build. 12 | */ 13 | const ignoreReactDevToolsPlugin = { 14 | name: "ignore-react-devtools", 15 | setup(build) { 16 | // When an import for 'react-devtools-core' is encountered, 17 | // return an empty module. 18 | build.onResolve({ filter: /^react-devtools-core$/ }, (args) => { 19 | return { path: args.path, namespace: "ignore-devtools" }; 20 | }); 21 | build.onLoad({ filter: /.*/, namespace: "ignore-devtools" }, () => { 22 | return { contents: "", loader: "js" }; 23 | }); 24 | }, 25 | }; 26 | 27 | // ---------------------------------------------------------------------------- 28 | // Build mode detection (production vs development) 29 | // 30 | // • production (default): minified, external telemetry shebang handling. 31 | // • development (--dev|NODE_ENV=development|CODEX_DEV=1): 32 | // – no minification 33 | // – inline source maps for better stacktraces 34 | // – shebang tweaked to enable Node's source‑map support at runtime 35 | // ---------------------------------------------------------------------------- 36 | 37 | const isDevBuild = 38 | process.argv.includes("--dev") || 39 | process.env.CODEX_DEV === "1" || 40 | process.env.NODE_ENV === "development"; 41 | 42 | const plugins = [ignoreReactDevToolsPlugin]; 43 | 44 | // Build Hygiene, ensure we drop previous dist dir and any leftover files 45 | const outPath = path.resolve(OUT_DIR); 46 | if (fs.existsSync(outPath)) { 47 | fs.rmSync(outPath, { recursive: true, force: true }); 48 | } 49 | 50 | // Add a shebang that enables source‑map support for dev builds so that stack 51 | // traces point to the original TypeScript lines without requiring callers to 52 | // remember to set NODE_OPTIONS manually. 53 | if (isDevBuild) { 54 | const devShebangLine = 55 | "#!/usr/bin/env -S NODE_OPTIONS=--enable-source-maps node\n"; 56 | const devShebangPlugin = { 57 | name: "dev-shebang", 58 | setup(build) { 59 | build.onEnd(async () => { 60 | const outFile = path.resolve(isDevBuild ? `${OUT_DIR}/cli-dev.js` : `${OUT_DIR}/cli.js`); 61 | let code = await fs.promises.readFile(outFile, "utf8"); 62 | if (code.startsWith("#!")) { 63 | code = code.replace(/^#!.*\n/, devShebangLine); 64 | await fs.promises.writeFile(outFile, code, "utf8"); 65 | } 66 | }); 67 | }, 68 | }; 69 | plugins.push(devShebangPlugin); 70 | } 71 | 72 | esbuild 73 | .build({ 74 | entryPoints: ["src/cli.tsx"], 75 | bundle: true, 76 | format: "esm", 77 | platform: "node", 78 | tsconfig: "tsconfig.json", 79 | outfile: isDevBuild ? `${OUT_DIR}/cli-dev.js` : `${OUT_DIR}/cli.js`, 80 | minify: !isDevBuild, 81 | sourcemap: isDevBuild ? "inline" : true, 82 | plugins, 83 | inject: ["./require-shim.js"], 84 | }) 85 | .catch(() => process.exit(1)); 86 | -------------------------------------------------------------------------------- /codex-cli/examples/README.md: -------------------------------------------------------------------------------- 1 | # Quick start examples 2 | 3 | This directory bundles some self‑contained examples using the Codex CLI. If you have never used the Codex CLI before, and want to see it complete a sample task, start with running **camerascii**. You'll see your webcam feed turned into animated ASCII art in a few minutes. 4 | 5 | If you want to get started using the Codex CLI directly, skip this and refer to the prompting guide. 6 | 7 | ## Structure 8 | 9 | Each example contains the following: 10 | ``` 11 | example‑name/ 12 | ├── run.sh # helper script that launches a new Codex session for the task 13 | ├── task.yaml # task spec containing a prompt passed to Codex 14 | ├── template/ # (optional) starter files copied into each run 15 | └── runs/ # work directories created by run.sh 16 | ``` 17 | 18 | **run.sh**: a convenience wrapper that does three things: 19 | - Creates `runs/run_N`, where *N* is the number of a run. 20 | - Copies the contents of `template/` into that folder (if present). 21 | - Launches the Codex CLI with the description from `task.yaml`. 22 | 23 | **template/**: any existing files or markdown instructions you would like Codex to see before it starts working. 24 | 25 | **runs/**: the directories produced by `run.sh`. 26 | 27 | ## Running an example 28 | 29 | 1. **Run the helper script**: 30 | ``` 31 | cd camerascii 32 | ./run.sh 33 | ``` 34 | 2. **Interact with the Codex CLI**: the CLI will open with the prompt: “*Take a look at the screenshot details and implement a webpage that uses a webcam to style the video feed accordingly…*” Confirm the commands Codex CLI requests to generate `index.html`. 35 | 36 | 3. **Check its work**: when Codex is done, open ``runs/run_1/index.html`` in a browser. Your webcam feed should now be rendered as a cascade of ASCII glyphs. If the outcome isn't what you expect, try running it again, or adjust the task prompt. 37 | 38 | 39 | ## Other examples 40 | Besides **camerascii**, you can experiment with: 41 | 42 | - **build‑codex‑demo**: recreate the original 2021 Codex YouTube demo. 43 | - **impossible‑pong**: where Codex creates more difficult levels. 44 | - **prompt‑analyzer**: make a data science app for clustering [prompts](https://github.com/f/awesome-chatgpt-prompts). 45 | -------------------------------------------------------------------------------- /codex-cli/examples/build-codex-demo/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, 4 | # then launch Codex with the task description from task.yaml. 5 | # 6 | # Usage: 7 | # ./run.sh # Prompts to confirm new run 8 | # ./run.sh --auto-confirm # Skips confirmation 9 | # 10 | # Assumes: 11 | # - yq and jq are installed 12 | # - ../task.yaml exists (with .name and .description fields) 13 | # - ../template/ exists (optional, for bootstrapping new runs) 14 | 15 | # Enable auto-confirm mode if flag is passed 16 | auto_mode=false 17 | [[ "$1" == "--auto-confirm" ]] && auto_mode=true 18 | 19 | # Move into the working directory 20 | cd runs || exit 1 21 | 22 | # Grab task name for logging 23 | task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') 24 | echo "Checking for runs for task: $task_name" 25 | 26 | # Find existing run_N directories 27 | shopt -s nullglob 28 | run_dirs=(run_[0-9]*) 29 | shopt -u nullglob 30 | 31 | if [ ${#run_dirs[@]} -eq 0 ]; then 32 | echo "There are 0 runs." 33 | new_run_number=1 34 | else 35 | max_run_number=0 36 | for d in "${run_dirs[@]}"; do 37 | [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} 38 | done 39 | new_run_number=$((max_run_number + 1)) 40 | echo "There are $max_run_number runs." 41 | fi 42 | 43 | # Confirm creation unless in auto mode 44 | if [ "$auto_mode" = false ]; then 45 | read -p "Create run_$new_run_number? (Y/N): " choice 46 | [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 47 | fi 48 | 49 | # Create the run directory 50 | mkdir "run_$new_run_number" 51 | 52 | # Check if the template directory exists and copy its contents 53 | if [ -d "../template" ]; then 54 | cp -r ../template/* "run_$new_run_number" 55 | echo "Initialized run_$new_run_number from template/" 56 | else 57 | echo "Template directory does not exist. Skipping initialization from template." 58 | fi 59 | 60 | cd "run_$new_run_number" 61 | 62 | # Launch Codex 63 | echo "Launching..." 64 | description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') 65 | codex "$description" 66 | -------------------------------------------------------------------------------- /codex-cli/examples/build-codex-demo/runs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymichael/open-codex/88bb03b3e5fcfcb6a39fbb76f510486cea5cddd9/codex-cli/examples/build-codex-demo/runs/.gitkeep -------------------------------------------------------------------------------- /codex-cli/examples/build-codex-demo/task.yaml: -------------------------------------------------------------------------------- 1 | name: "build-codex-demo" 2 | description: | 3 | I want you to reimplement the original OpenAI Codex demo. 4 | 5 | Functionality: 6 | - User types a prompt and hits enter to send 7 | - The prompt is added to the conversation history 8 | - The backend calls the OpenAI API with stream: true 9 | - Tokens are streamed back and appended to the code viewer 10 | - Syntax highlighting updates in real time 11 | - When a full HTML file is received, it is rendered in a sandboxed iframe 12 | - The iframe replaces the previous preview with the new HTML after the stream is complete (i.e. keep the old preview until a new stream is complete) 13 | - Append each assistant and user message to preserve context across turns 14 | - Errors are displayed to user gracefully 15 | - Ensure there is a fixed layout is responsive and faithful to the screenshot design 16 | - Be sure to parse the output from OpenAI call to strip the ```html tags code is returned within 17 | - Use the system prompt shared in the API call below to ensure the AI only returns HTML 18 | 19 | Support a simple local backend that can: 20 | - Read local env for OPENAI_API_KEY 21 | - Expose an endpoint that streams completions from OpenAI 22 | - Backend should be a simple node.js app 23 | - App should be easy to run locally for development and testing 24 | - Minimal setup preferred — keep dependencies light unless justified 25 | 26 | Description of layout and design: 27 | - Two stacked panels, vertically aligned: 28 | - Top Panel: Main interactive area with two main parts 29 | - Left Side: Visual output canvas. Mostly blank space with a small image preview in the upper-left 30 | - Right Side: Code display area 31 | - Light background with code shown in a monospace font 32 | - Comments in green; code aligns vertically like an IDE/snippet view 33 | - Bottom Panel: Prompt/command bar 34 | - A single-line text box with a placeholder prompt 35 | - A green arrow (submit button) on the right side 36 | - Scrolling should only be supported in the code editor and output canvas 37 | 38 | Visual style 39 | - Minimalist UI, light and clean 40 | - Neutral white/gray background 41 | - Subtle shadow or border around both panels, giving them card-like elevation 42 | - Code section is color-coded, likely for syntax highlighting 43 | - Interactive feel with the text input styled like a chat/message interface 44 | 45 | Here's the latest OpenAI API and prompt to use: 46 | ``` 47 | import OpenAI from "openai"; 48 | 49 | const openai = new OpenAI({ 50 | apiKey: process.env.OPENAI_API_KEY, 51 | }); 52 | 53 | const response = await openai.responses.create({ 54 | model: "gpt-4.1", 55 | input: [ 56 | { 57 | "role": "system", 58 | "content": [ 59 | { 60 | "type": "input_text", 61 | "text": "You are a coding agent that specializes in frontend code. Whenever you are prompted, return only the full HTML file." 62 | } 63 | ] 64 | } 65 | ], 66 | text: { 67 | "format": { 68 | "type": "text" 69 | } 70 | }, 71 | reasoning: {}, 72 | tools: [], 73 | temperature: 1, 74 | top_p: 1 75 | }); 76 | 77 | console.log(response.output_text); 78 | ``` 79 | Additional things to note: 80 | - Strip any html and tags from the OpenAI response before rendering 81 | - Assume the OpenAI API model response always wraps HTML in markdown-style triple backticks like ```html ``` 82 | - The display code window should have syntax highlighting and line numbers. 83 | - Make sure to only display the code, not the backticks or ```html that wrap the code from the model. 84 | - Do not inject raw markdown; only parse and insert pure HTML into the iframe 85 | - Only the code viewer and output panel should scroll 86 | - Keep the previous preview visible until the full new HTML has streamed in 87 | 88 | Add a README.md with what you've implemented and how to run it. 89 | -------------------------------------------------------------------------------- /codex-cli/examples/camerascii/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, 4 | # then launch Codex with the task description from task.yaml. 5 | # 6 | # Usage: 7 | # ./run.sh # Prompts to confirm new run 8 | # ./run.sh --auto-confirm # Skips confirmation 9 | # 10 | # Assumes: 11 | # - yq and jq are installed 12 | # - ../task.yaml exists (with .name and .description fields) 13 | # - ../template/ exists (optional, for bootstrapping new runs) 14 | 15 | # Enable auto-confirm mode if flag is passed 16 | auto_mode=false 17 | [[ "$1" == "--auto-confirm" ]] && auto_mode=true 18 | 19 | # Create the runs directory if it doesn't exist 20 | mkdir -p runs 21 | 22 | # Move into the working directory 23 | cd runs || exit 1 24 | 25 | # Grab task name for logging 26 | task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') 27 | echo "Checking for runs for task: $task_name" 28 | 29 | # Find existing run_N directories 30 | shopt -s nullglob 31 | run_dirs=(run_[0-9]*) 32 | shopt -u nullglob 33 | 34 | if [ ${#run_dirs[@]} -eq 0 ]; then 35 | echo "There are 0 runs." 36 | new_run_number=1 37 | else 38 | max_run_number=0 39 | for d in "${run_dirs[@]}"; do 40 | [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} 41 | done 42 | new_run_number=$((max_run_number + 1)) 43 | echo "There are $max_run_number runs." 44 | fi 45 | 46 | # Confirm creation unless in auto mode 47 | if [ "$auto_mode" = false ]; then 48 | read -p "Create run_$new_run_number? (Y/N): " choice 49 | [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 50 | fi 51 | 52 | # Create the run directory 53 | mkdir "run_$new_run_number" 54 | 55 | # Check if the template directory exists and copy its contents 56 | if [ -d "../template" ]; then 57 | cp -r ../template/* "run_$new_run_number" 58 | echo "Initialized run_$new_run_number from template/" 59 | else 60 | echo "Template directory does not exist. Skipping initialization from template." 61 | fi 62 | 63 | cd "run_$new_run_number" 64 | 65 | # Launch Codex 66 | echo "Launching..." 67 | description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') 68 | codex "$description" 69 | -------------------------------------------------------------------------------- /codex-cli/examples/camerascii/runs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymichael/open-codex/88bb03b3e5fcfcb6a39fbb76f510486cea5cddd9/codex-cli/examples/camerascii/runs/.gitkeep -------------------------------------------------------------------------------- /codex-cli/examples/camerascii/task.yaml: -------------------------------------------------------------------------------- 1 | name: "camerascii" 2 | description: | 3 | Take a look at the screenshot details and implement a webpage that uses webcam 4 | to style the video feed accordingly (i.e. as ASCII art). Add some of the relevant features 5 | from the screenshot to the webpage in index.html. 6 | -------------------------------------------------------------------------------- /codex-cli/examples/camerascii/template/screenshot_details.md: -------------------------------------------------------------------------------- 1 | ### Screenshot Description 2 | 3 | The image is a full–page screenshot of a single post on the social‑media site X (formerly Twitter). 4 | 5 | 1. **Header row** 6 | * At the very top‑left is a small circular avatar. The photo shows the side profile of a person whose face is softly lit in bluish‑purple tones; only the head and part of the neck are visible. 7 | * In the far upper‑right corner sit two standard X / Twitter interface icons: a circle containing a diagonal line (the “Mute / Block” indicator) and a three‑dot overflow menu. 8 | 9 | 2. **Tweet body text** 10 | * Below the header, in regular type, the author writes: 11 | 12 | “Okay, OpenAI’s o3 is insane. Spent an hour messing with it and built an image‑to‑ASCII art converter, the exact tool I’ve always wanted. And it works so well” 13 | 14 | 3. **Embedded media** 15 | * The majority of the screenshot is occupied by an embedded 12‑second video of the converter UI. The video window has rounded corners and a dark theme. 16 | * **Left panel (tool controls)** – a slim vertical sidebar with the following labeled sections and blue–accented UI controls: 17 | * Theme selector (“Dark” is chosen). 18 | * A small checkbox labeled “Ignore White”. 19 | * **Upload Image** button area that shows the chosen file name. 20 | * **Image Processing** sliders: 21 | * “ASCII Width” (value ≈ 143) 22 | * “Brightness” (‑65) 23 | * “Contrast” (58) 24 | * “Blur (px)” (0.5) 25 | * A square checkbox for “Invert Colors”. 26 | * **Dithering** subsection with a checkbox (“Enable Dithering”) and a dropdown for the algorithm (value: “Noise”). 27 | * **Character Set** dropdown (value: “Detailed (Default)”). 28 | * **Display** slider labeled “Zoom (%)” (value ≈ 170) and a “Reset” button. 29 | 30 | * **Main preview area (right side)** – a dark gray canvas that renders the selected image as white ASCII characters. The preview clearly depicts a stylized **palm tree**: a skinny trunk rises from the bottom centre, and a crown of splayed fronds fills the upper right quadrant. 31 | * A small black badge showing **“0:12”** overlays the bottom‑left corner of the media frame, indicating the video’s duration. 32 | * In the top‑right area of the media window are two pill‑shaped buttons: a heart‑shaped “Save” button and a cog‑shaped “Settings” button. 33 | 34 | Overall, the screenshot shows the user excitedly announcing the success of their custom “Image to ASCII” converter created with OpenAI’s “o3”, accompanied by a short video demonstration of the tool converting a palm‑tree photo into ASCII art. 35 | -------------------------------------------------------------------------------- /codex-cli/examples/impossible-pong/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, 4 | # then launch Codex with the task description from task.yaml. 5 | # 6 | # Usage: 7 | # ./run.sh # Prompts to confirm new run 8 | # ./run.sh --auto-confirm # Skips confirmation 9 | # 10 | # Assumes: 11 | # - yq and jq are installed 12 | # - ../task.yaml exists (with .name and .description fields) 13 | # - ../template/ exists (optional, for bootstrapping new runs) 14 | 15 | # Enable auto-confirm mode if flag is passed 16 | auto_mode=false 17 | [[ "$1" == "--auto-confirm" ]] && auto_mode=true 18 | 19 | # Create the runs directory if it doesn't exist 20 | mkdir -p runs 21 | 22 | # Move into the working directory 23 | cd runs || exit 1 24 | 25 | # Grab task name for logging 26 | task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') 27 | echo "Checking for runs for task: $task_name" 28 | 29 | # Find existing run_N directories 30 | shopt -s nullglob 31 | run_dirs=(run_[0-9]*) 32 | shopt -u nullglob 33 | 34 | if [ ${#run_dirs[@]} -eq 0 ]; then 35 | echo "There are 0 runs." 36 | new_run_number=1 37 | else 38 | max_run_number=0 39 | for d in "${run_dirs[@]}"; do 40 | [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} 41 | done 42 | new_run_number=$((max_run_number + 1)) 43 | echo "There are $max_run_number runs." 44 | fi 45 | 46 | # Confirm creation unless in auto mode 47 | if [ "$auto_mode" = false ]; then 48 | read -p "Create run_$new_run_number? (Y/N): " choice 49 | [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 50 | fi 51 | 52 | # Create the run directory 53 | mkdir "run_$new_run_number" 54 | 55 | # Check if the template directory exists and copy its contents 56 | if [ -d "../template" ]; then 57 | cp -r ../template/* "run_$new_run_number" 58 | echo "Initialized run_$new_run_number from template/" 59 | else 60 | echo "Template directory does not exist. Skipping initialization from template." 61 | fi 62 | 63 | cd "run_$new_run_number" 64 | 65 | # Launch Codex 66 | echo "Launching..." 67 | description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') 68 | codex "$description" 69 | -------------------------------------------------------------------------------- /codex-cli/examples/impossible-pong/runs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymichael/open-codex/88bb03b3e5fcfcb6a39fbb76f510486cea5cddd9/codex-cli/examples/impossible-pong/runs/.gitkeep -------------------------------------------------------------------------------- /codex-cli/examples/impossible-pong/task.yaml: -------------------------------------------------------------------------------- 1 | name: "impossible-pong" 2 | description: | 3 | Update index.html with the following features: 4 | - Add an overlayed styled popup to start the game on first load 5 | - Between each point, show a 3 second countdown (this should be skipped if a player wins) 6 | - After each game the AI wins, display text at the bottom of the screen with lighthearted insults for the player 7 | - Add a leaderboard to the right of the court that shows how many games each player has won. 8 | - When a player wins, a styled popup appears with the winner's name and the option to play again. The leaderboard should update. 9 | - Add an "even more insane" difficulty mode that adds spin to the ball that makes it harder to predict. 10 | - Add an "even more(!!) insane" difficulty mode where the ball does a spin mid court and then picks a random (reasonable) direction to go in (this should only advantage the AI player) 11 | - Let the user choose which difficulty mode they want to play in on the popup that appears when the game starts. 12 | -------------------------------------------------------------------------------- /codex-cli/examples/prompt-analyzer/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, 4 | # then launch Codex with the task description from task.yaml. 5 | # 6 | # Usage: 7 | # ./run.sh # Prompts to confirm new run 8 | # ./run.sh --auto-confirm # Skips confirmation 9 | # 10 | # Assumes: 11 | # - yq and jq are installed 12 | # - ../task.yaml exists (with .name and .description fields) 13 | # - ../template/ exists (optional, for bootstrapping new runs) 14 | 15 | # Enable auto-confirm mode if flag is passed 16 | auto_mode=false 17 | [[ "$1" == "--auto-confirm" ]] && auto_mode=true 18 | 19 | # Create the runs directory if it doesn't exist 20 | mkdir -p runs 21 | 22 | # Move into the working directory 23 | cd runs || exit 1 24 | 25 | # Grab task name for logging 26 | task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') 27 | echo "Checking for runs for task: $task_name" 28 | 29 | # Find existing run_N directories 30 | shopt -s nullglob 31 | run_dirs=(run_[0-9]*) 32 | shopt -u nullglob 33 | 34 | if [ ${#run_dirs[@]} -eq 0 ]; then 35 | echo "There are 0 runs." 36 | new_run_number=1 37 | else 38 | max_run_number=0 39 | for d in "${run_dirs[@]}"; do 40 | [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} 41 | done 42 | new_run_number=$((max_run_number + 1)) 43 | echo "There are $max_run_number runs." 44 | fi 45 | 46 | # Confirm creation unless in auto mode 47 | if [ "$auto_mode" = false ]; then 48 | read -p "Create run_$new_run_number? (Y/N): " choice 49 | [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 50 | fi 51 | 52 | # Create the run directory 53 | mkdir "run_$new_run_number" 54 | 55 | # Check if the template directory exists and copy its contents 56 | if [ -d "../template" ]; then 57 | cp -r ../template/* "run_$new_run_number" 58 | echo "Initialized run_$new_run_number from template/" 59 | else 60 | echo "Template directory does not exist. Skipping initialization from template." 61 | fi 62 | 63 | cd "run_$new_run_number" 64 | 65 | # Launch Codex 66 | echo "Launching..." 67 | description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') 68 | codex "$description" 69 | -------------------------------------------------------------------------------- /codex-cli/examples/prompt-analyzer/runs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymichael/open-codex/88bb03b3e5fcfcb6a39fbb76f510486cea5cddd9/codex-cli/examples/prompt-analyzer/runs/.gitkeep -------------------------------------------------------------------------------- /codex-cli/examples/prompt-analyzer/task.yaml: -------------------------------------------------------------------------------- 1 | name: "prompt-analyzer" 2 | description: | 3 | I have some existing work here (embedding prompts, clustering them, generating 4 | summaries with GPT). I want to make it more interactive and reusable. 5 | 6 | Objective: create an interactive cluster explorer 7 | - Build a lightweight streamlit app UI 8 | - Allow users to upload a CSV of prompts 9 | - Display clustered prompts with auto-generated cluster names and summaries 10 | - Click "cluster" and see progress stream in a small window (primarily for aesthetic reasons) 11 | - Let users browse examples by cluster, view outliers, and inspect individual prompts 12 | - See generated analysis rendered in the app, along with the plots displayed nicely 13 | - Support selecting clustering algorithms (e.g. DBSCAN, KMeans, etc) and "recluster" 14 | - Include token count + histogram of prompt lengths 15 | - Add interactive filters in UI (e.g. filter by token length, keyword, or cluster) 16 | 17 | When you're done, update the README.md with a changelog and instructions for how to run the app. 18 | -------------------------------------------------------------------------------- /codex-cli/examples/prompt-analyzer/template/README.md: -------------------------------------------------------------------------------- 1 | # Prompt‑Clustering Utility 2 | 3 | This repository contains a small utility (`cluster_prompts.py`) that embeds a 4 | list of prompts with the OpenAI Embedding API, discovers natural groupings with 5 | unsupervised clustering, lets ChatGPT name & describe each cluster and finally 6 | produces a concise Markdown report plus a couple of diagnostic plots. 7 | 8 | The default input file (`prompts.csv`) ships with the repo so you can try the 9 | script immediately, but you can of course point it at your own file. 10 | 11 | --- 12 | 13 | ## 1. Setup 14 | 15 | 1. Install the Python dependencies (preferably inside a virtual env): 16 | 17 | ```bash 18 | pip install pandas numpy scikit-learn matplotlib openai 19 | ``` 20 | 21 | 2. Export your OpenAI API key (**required**): 22 | 23 | ```bash 24 | export OPENAI_API_KEY="sk‑..." 25 | ``` 26 | 27 | --- 28 | 29 | ## 2. Basic usage 30 | 31 | ```bash 32 | # Minimal command – runs on prompts.csv and writes analysis.md + plots/ 33 | python cluster_prompts.py 34 | ``` 35 | 36 | This will 37 | 38 | * create embeddings with the `text-embedding-3-small` model,  39 | * pick a suitable number *k* via silhouette score (K‑Means), 40 | * ask `gpt‑4o‑mini` to label & describe each cluster, 41 | * store the results in `analysis.md`, 42 | * and save two plots to `plots/` (`cluster_sizes.png` and `tsne.png`). 43 | 44 | The script prints a short success message once done. 45 | 46 | --- 47 | 48 | ## 3. Command‑line options 49 | 50 | | flag | default | description | 51 | |------|---------|-------------| 52 | | `--csv` | `prompts.csv` | path to the input CSV (must contain a `prompt` column; an `act` column is used as context if present) | 53 | | `--cache` | _(none)_ | embed­ding cache path (JSON). Speeds up repeated runs – new texts are appended automatically. | 54 | | `--cluster-method` | `kmeans` | `kmeans` (with automatic *k*) or `dbscan` | 55 | | `--k-max` | `10` | upper bound for *k* when `kmeans` is selected | 56 | | `--dbscan-min-samples` | `3` | min samples parameter for DBSCAN | 57 | | `--embedding-model` | `text-embedding-3-small` | any OpenAI embedding model | 58 | | `--chat-model` | `gpt-4o-mini` | chat model used to generate cluster names / descriptions | 59 | | `--output-md` | `analysis.md` | where to write the Markdown report | 60 | | `--plots-dir` | `plots` | directory for generated PNGs | 61 | 62 | Example with customised options: 63 | 64 | ```bash 65 | python cluster_prompts.py \ 66 | --csv my_prompts.csv \ 67 | --cache .cache/embeddings.json \ 68 | --cluster-method dbscan \ 69 | --embedding-model text-embedding-3-large \ 70 | --chat-model gpt-4o \ 71 | --output-md my_analysis.md \ 72 | --plots-dir my_plots 73 | ``` 74 | 75 | --- 76 | 77 | ## 4. Interpreting the output 78 | 79 | ### analysis.md 80 | 81 | * Overview table: cluster label, generated name, member count and description. 82 | * Detailed section for every cluster with five representative example prompts. 83 | * Separate lists for 84 | * **Noise / outliers** (label `‑1` when DBSCAN is used) and 85 | * **Potentially ambiguous prompts** (only with K‑Means) – these are items that 86 | lie almost equally close to two centroids and might belong to multiple 87 | groups. 88 | 89 | ### plots/cluster_sizes.png 90 | 91 | Quick bar‑chart visualisation of how many prompts ended up in each cluster. 92 | 93 | --- 94 | 95 | ## 5. Troubleshooting 96 | 97 | * **Rate‑limits / quota errors** – lower the number of prompts per run or switch 98 | to a larger quota account. 99 | * **Authentication errors** – make sure `OPENAI_API_KEY` is exported in the 100 | shell where you run the script. 101 | * **Inadequate clusters** – try the other clustering method, adjust `--k-max` 102 | or tune DBSCAN parameters (`eps` range is inferred, `min_samples` exposed via 103 | CLI). 104 | -------------------------------------------------------------------------------- /codex-cli/examples/prompt-analyzer/template/analysis.md: -------------------------------------------------------------------------------- 1 | # Prompt Clustering Report 2 | 3 | Generated by `cluster_prompts.py` – 2025-04-16 4 | 5 | 6 | ## Overview 7 | 8 | * Total prompts: **213** 9 | * Clustering method: **kmeans** 10 | * k (K‑Means): **2** 11 | * Silhouette score: **0.042** 12 | * Final clusters (excluding noise): **2** 13 | 14 | 15 | | label | name | #prompts | description | 16 | |-------|------|---------:|-------------| 17 | | 0 | Creative Guidance Roles | 121 | This cluster encompasses a variety of roles where individuals provide expert advice, suggestions, and creative ideas across different fields. Each role, be it interior decorator, comedian, IT architect, or artist advisor, focuses on enhancing the expertise and creativity of others by tailoring advice to specific requests and contexts. | 18 | | 1 | Role Customization Requests | 92 | This cluster contains various requests for role-specific assistance across different domains, including web development, language processing, IT troubleshooting, and creative endeavors. Each snippet illustrates a unique role that a user wishes to engage with, focusing on specific tasks without requiring explanations. | 19 | 20 | --- 21 | ## Plots 22 | 23 | The directory `plots/` contains a bar chart of the cluster sizes and a t‑SNE scatter plot coloured by cluster. 24 | -------------------------------------------------------------------------------- /codex-cli/examples/prompt-analyzer/template/analysis_dbscan.md: -------------------------------------------------------------------------------- 1 | # Prompt Clustering Report 2 | 3 | Generated by `cluster_prompts.py` – 2025-04-16 4 | 5 | 6 | ## Overview 7 | 8 | * Total prompts: **213** 9 | * Clustering method: **dbscan** 10 | * Final clusters (excluding noise): **1** 11 | 12 | 13 | | label | name | #prompts | description | 14 | |-------|------|---------:|-------------| 15 | | -1 | Noise / Outlier | 10 | Prompts that do not cleanly belong to any cluster. | 16 | | 0 | Role Simulation Tasks | 203 | This cluster consists of varied role-playing scenarios where users request an AI to assume specific professional roles, such as composer, dream interpreter, doctor, or IT architect. Each snippet showcases tasks that involve creating content, providing advice, or performing analytical functions based on user-defined themes or prompts. | 17 | 18 | --- 19 | 20 | ## Plots 21 | 22 | The directory `plots/` contains a bar chart of the cluster sizes and a t‑SNE scatter plot coloured by cluster. 23 | -------------------------------------------------------------------------------- /codex-cli/examples/prompt-analyzer/template/plots/cluster_sizes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymichael/open-codex/88bb03b3e5fcfcb6a39fbb76f510486cea5cddd9/codex-cli/examples/prompt-analyzer/template/plots/cluster_sizes.png -------------------------------------------------------------------------------- /codex-cli/examples/prompt-analyzer/template/plots/tsne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymichael/open-codex/88bb03b3e5fcfcb6a39fbb76f510486cea5cddd9/codex-cli/examples/prompt-analyzer/template/plots/tsne.png -------------------------------------------------------------------------------- /codex-cli/examples/prompt-analyzer/template/plots_dbscan/cluster_sizes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymichael/open-codex/88bb03b3e5fcfcb6a39fbb76f510486cea5cddd9/codex-cli/examples/prompt-analyzer/template/plots_dbscan/cluster_sizes.png -------------------------------------------------------------------------------- /codex-cli/examples/prompt-analyzer/template/plots_dbscan/tsne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymichael/open-codex/88bb03b3e5fcfcb6a39fbb76f510486cea5cddd9/codex-cli/examples/prompt-analyzer/template/plots_dbscan/tsne.png -------------------------------------------------------------------------------- /codex-cli/ignore-react-devtools-plugin.js: -------------------------------------------------------------------------------- 1 | // ignore-react-devtools-plugin.js 2 | const ignoreReactDevToolsPlugin = { 3 | name: "ignore-react-devtools", 4 | setup(build) { 5 | // When an import for 'react-devtools-core' is encountered, 6 | // return an empty module. 7 | build.onResolve({ filter: /^react-devtools-core$/ }, (args) => { 8 | return { path: args.path, namespace: "ignore-devtools" }; 9 | }); 10 | build.onLoad({ filter: /.*/, namespace: "ignore-devtools" }, () => { 11 | return { contents: "", loader: "js" }; 12 | }); 13 | }, 14 | }; 15 | 16 | module.exports = ignoreReactDevToolsPlugin; 17 | -------------------------------------------------------------------------------- /codex-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-codex", 3 | "version": "0.1.30", 4 | "license": "Apache-2.0", 5 | "bin": { 6 | "open-codex": "dist/cli.js" 7 | }, 8 | "type": "module", 9 | "engines": { 10 | "node": ">=22" 11 | }, 12 | "scripts": { 13 | "format": "prettier --check src tests", 14 | "format:fix": "prettier --write src tests", 15 | "dev": "tsc --watch", 16 | "lint": "eslint src tests --ext ts --ext tsx --report-unused-disable-directives --max-warnings 0", 17 | "lint:fix": "eslint src tests --ext ts --ext tsx --fix", 18 | "test": "vitest run", 19 | "test:watch": "vitest --watch", 20 | "typecheck": "tsc --noEmit", 21 | "build": "node build.mjs", 22 | "build:dev": "NODE_ENV=development node build.mjs --dev && NODE_OPTIONS=--enable-source-maps node dist/cli-dev.js", 23 | "release:readme": "cp ../README.md ./README.md", 24 | "release:version": "npm run build:dev -- -a full-auto 'update the CLI_VERSION in codex-cli/src/utils/session.ts and the version in package.json to the next value.'", 25 | "release": "npm run release:readme && npm run release:version && npm run build && npm publish" 26 | }, 27 | "files": [ 28 | "dist" 29 | ], 30 | "dependencies": { 31 | "@inkjs/ui": "^2.0.0", 32 | "chalk": "^5.2.0", 33 | "diff": "^7.0.0", 34 | "dotenv": "^16.1.4", 35 | "fast-deep-equal": "^3.1.3", 36 | "file-type": "^20.1.0", 37 | "ink": "^5.2.0", 38 | "marked": "^15.0.7", 39 | "marked-terminal": "^7.3.0", 40 | "meow": "^13.2.0", 41 | "open": "^10.1.0", 42 | "openai": "^4.89.0", 43 | "react": "^18.2.0", 44 | "shell-quote": "^1.8.2", 45 | "to-rotated": "^1.0.0", 46 | "use-interval": "1.4.0" 47 | }, 48 | "devDependencies": { 49 | "@eslint/js": "^9.22.0", 50 | "@types/diff": "^7.0.2", 51 | "@types/js-yaml": "^4.0.9", 52 | "@types/marked-terminal": "^6.1.1", 53 | "@types/react": "^18.0.32", 54 | "@types/shell-quote": "^1.7.5", 55 | "@types/which": "^3.0.4", 56 | "@typescript-eslint/eslint-plugin": "^7.18.0", 57 | "@typescript-eslint/parser": "^7.18.0", 58 | "boxen": "^8.0.1", 59 | "esbuild": "^0.25.2", 60 | "eslint-plugin-import": "^2.31.0", 61 | "eslint-plugin-react": "^7.32.2", 62 | "eslint-plugin-react-hooks": "^4.6.0", 63 | "eslint-plugin-react-refresh": "^0.4.19", 64 | "ink-testing-library": "^3.0.0", 65 | "prettier": "^2.8.7", 66 | "punycode": "^2.3.1", 67 | "ts-node": "^10.9.1", 68 | "typescript": "^5.0.3", 69 | "vitest": "^3.0.9", 70 | "whatwg-url": "^14.2.0", 71 | "which": "^5.0.0" 72 | }, 73 | "resolutions": { 74 | "braces": "^3.0.3", 75 | "micromatch": "^4.0.8", 76 | "semver": "^7.7.1" 77 | }, 78 | "overrides": { 79 | "punycode": "^2.3.1" 80 | }, 81 | "repository": { 82 | "type": "git", 83 | "url": "https://github.com/ymichael/codex" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /codex-cli/require-shim.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is necessary because we have transitive dependencies on CommonJS modules 3 | * that use require() conditionally: 4 | * 5 | * https://github.com/tapjs/signal-exit/blob/v3.0.7/index.js#L26-L27 6 | * 7 | * This is not compatible with ESM, so we need to shim require() to use the 8 | * CommonJS module loader. 9 | */ 10 | import { createRequire } from "module"; 11 | globalThis.require = createRequire(import.meta.url); 12 | -------------------------------------------------------------------------------- /codex-cli/scripts/build_container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | SCRIPT_DIR=$(realpath "$(dirname "$0")") 6 | trap "popd >> /dev/null" EXIT 7 | pushd "$SCRIPT_DIR/.." >> /dev/null || { 8 | echo "Error: Failed to change directory to $SCRIPT_DIR/.." 9 | exit 1 10 | } 11 | npm install 12 | npm run build 13 | rm -rf ./dist/openai-codex-*.tgz 14 | npm pack --pack-destination ./dist 15 | mv ./dist/openai-codex-*.tgz ./dist/codex.tgz 16 | docker build -t codex -f "./Dockerfile" . 17 | -------------------------------------------------------------------------------- /codex-cli/scripts/init_firewall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail # Exit on error, undefined vars, and pipeline failures 3 | IFS=$'\n\t' # Stricter word splitting 4 | 5 | # Flush existing rules and delete existing ipsets 6 | iptables -F 7 | iptables -X 8 | iptables -t nat -F 9 | iptables -t nat -X 10 | iptables -t mangle -F 11 | iptables -t mangle -X 12 | ipset destroy allowed-domains 2>/dev/null || true 13 | 14 | # First allow DNS and localhost before any restrictions 15 | # Allow outbound DNS 16 | iptables -A OUTPUT -p udp --dport 53 -j ACCEPT 17 | # Allow inbound DNS responses 18 | iptables -A INPUT -p udp --sport 53 -j ACCEPT 19 | # Allow localhost 20 | iptables -A INPUT -i lo -j ACCEPT 21 | iptables -A OUTPUT -o lo -j ACCEPT 22 | 23 | # Create ipset with CIDR support 24 | ipset create allowed-domains hash:net 25 | 26 | # Resolve and add other allowed domains 27 | for domain in \ 28 | "api.openai.com"; do 29 | echo "Resolving $domain..." 30 | ips=$(dig +short A "$domain") 31 | if [ -z "$ips" ]; then 32 | echo "ERROR: Failed to resolve $domain" 33 | exit 1 34 | fi 35 | 36 | while read -r ip; do 37 | if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 38 | echo "ERROR: Invalid IP from DNS for $domain: $ip" 39 | exit 1 40 | fi 41 | echo "Adding $ip for $domain" 42 | ipset add allowed-domains "$ip" 43 | done < <(echo "$ips") 44 | done 45 | 46 | # Get host IP from default route 47 | HOST_IP=$(ip route | grep default | cut -d" " -f3) 48 | if [ -z "$HOST_IP" ]; then 49 | echo "ERROR: Failed to detect host IP" 50 | exit 1 51 | fi 52 | 53 | HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/") 54 | echo "Host network detected as: $HOST_NETWORK" 55 | 56 | # Set up remaining iptables rules 57 | iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT 58 | iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT 59 | 60 | # Set default policies to DROP first 61 | iptables -P INPUT DROP 62 | iptables -P FORWARD DROP 63 | iptables -P OUTPUT DROP 64 | 65 | # First allow established connections for already approved traffic 66 | iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 67 | iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 68 | 69 | # Then allow only specific outbound traffic to allowed domains 70 | iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT 71 | 72 | # Append final REJECT rules for immediate error responses 73 | # For TCP traffic, send a TCP reset; for UDP, send ICMP port unreachable. 74 | iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset 75 | iptables -A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable 76 | iptables -A OUTPUT -p tcp -j REJECT --reject-with tcp-reset 77 | iptables -A OUTPUT -p udp -j REJECT --reject-with icmp-port-unreachable 78 | iptables -A FORWARD -p tcp -j REJECT --reject-with tcp-reset 79 | iptables -A FORWARD -p udp -j REJECT --reject-with icmp-port-unreachable 80 | 81 | echo "Firewall configuration complete" 82 | echo "Verifying firewall rules..." 83 | if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then 84 | echo "ERROR: Firewall verification failed - was able to reach https://example.com" 85 | exit 1 86 | else 87 | echo "Firewall verification passed - unable to reach https://example.com as expected" 88 | fi 89 | 90 | # Verify OpenAI API access 91 | if ! curl --connect-timeout 5 https://api.openai.com >/dev/null 2>&1; then 92 | echo "ERROR: Firewall verification failed - unable to reach https://api.openai.com" 93 | exit 1 94 | else 95 | echo "Firewall verification passed - able to reach https://api.openai.com as expected" 96 | fi 97 | -------------------------------------------------------------------------------- /codex-cli/scripts/run_in_container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Usage: 5 | # ./run_in_container.sh [--work_dir directory] "COMMAND" 6 | # 7 | # Examples: 8 | # ./run_in_container.sh --work_dir project/code "ls -la" 9 | # ./run_in_container.sh "echo Hello, world!" 10 | 11 | # Default the work directory to WORKSPACE_ROOT_DIR if not provided. 12 | WORK_DIR="${WORKSPACE_ROOT_DIR:-$(pwd)}" 13 | 14 | # Parse optional flag. 15 | if [ "$1" = "--work_dir" ]; then 16 | if [ -z "$2" ]; then 17 | echo "Error: --work_dir flag provided but no directory specified." 18 | exit 1 19 | fi 20 | WORK_DIR="$2" 21 | shift 2 22 | fi 23 | 24 | WORK_DIR=$(realpath "$WORK_DIR") 25 | 26 | # Generate a unique container name based on the normalized work directory 27 | CONTAINER_NAME="codex_$(echo "$WORK_DIR" | sed 's/\//_/g' | sed 's/[^a-zA-Z0-9_-]//g')" 28 | 29 | # Define cleanup to remove the container on script exit, ensuring no leftover containers 30 | cleanup() { 31 | docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true 32 | } 33 | # Trap EXIT to invoke cleanup regardless of how the script terminates 34 | trap cleanup EXIT 35 | 36 | # Ensure a command is provided. 37 | if [ "$#" -eq 0 ]; then 38 | echo "Usage: $0 [--work_dir directory] \"COMMAND\"" 39 | exit 1 40 | fi 41 | 42 | # Check if WORK_DIR is set. 43 | if [ -z "$WORK_DIR" ]; then 44 | echo "Error: No work directory provided and WORKSPACE_ROOT_DIR is not set." 45 | exit 1 46 | fi 47 | 48 | # Kill any existing container for the working directory using cleanup(), centralizing removal logic. 49 | cleanup 50 | 51 | # Run the container with the specified directory mounted at the same path inside the container. 52 | docker run --name "$CONTAINER_NAME" -d \ 53 | -e OPENAI_API_KEY \ 54 | --cap-add=NET_ADMIN \ 55 | --cap-add=NET_RAW \ 56 | -v "$WORK_DIR:/app$WORK_DIR" \ 57 | codex \ 58 | sleep infinity 59 | 60 | # Initialize the firewall inside the container. 61 | docker exec "$CONTAINER_NAME" bash -c "sudo /usr/local/bin/init_firewall.sh" 62 | 63 | # Execute the provided command in the container, ensuring it runs in the work directory. 64 | # We use a parameterized bash command to safely handle the command and directory. 65 | 66 | quoted_args="" 67 | for arg in "$@"; do 68 | quoted_args+=" $(printf '%q' "$arg")" 69 | done 70 | docker exec -it "$CONTAINER_NAME" bash -c "cd \"/app$WORK_DIR\" && codex --full-auto ${quoted_args}" 71 | -------------------------------------------------------------------------------- /codex-cli/src/app.tsx: -------------------------------------------------------------------------------- 1 | import type { ApprovalPolicy } from "./approvals"; 2 | import type { AppConfig } from "./utils/config"; 3 | import type { ChatCompletionMessageParam } from "openai/resources/chat/completions.mjs"; 4 | 5 | import TerminalChat from "./components/chat/terminal-chat"; 6 | import TerminalChatPastRollout from "./components/chat/terminal-chat-past-rollout"; 7 | import { checkInGit } from "./utils/check-in-git"; 8 | import { CLI_VERSION, type TerminalChatSession } from "./utils/session.js"; 9 | import { onExit } from "./utils/terminal"; 10 | import { ConfirmInput } from "@inkjs/ui"; 11 | import { Box, Text, useApp, useStdin } from "ink"; 12 | import React, { useMemo, useState } from "react"; 13 | 14 | export type AppRollout = { 15 | session: TerminalChatSession; 16 | items: Array; 17 | }; 18 | 19 | type Props = { 20 | prompt?: string; 21 | config: AppConfig; 22 | imagePaths?: Array; 23 | rollout?: AppRollout; 24 | approvalPolicy: ApprovalPolicy; 25 | fullStdout: boolean; 26 | }; 27 | 28 | export default function App({ 29 | prompt, 30 | config, 31 | rollout, 32 | imagePaths, 33 | approvalPolicy, 34 | fullStdout, 35 | }: Props): JSX.Element { 36 | const app = useApp(); 37 | const [accepted, setAccepted] = useState(() => false); 38 | const [cwd, inGitRepo] = useMemo( 39 | () => [process.cwd(), checkInGit(process.cwd())], 40 | [], 41 | ); 42 | const { internal_eventEmitter } = useStdin(); 43 | internal_eventEmitter.setMaxListeners(20); 44 | 45 | if (rollout) { 46 | return ( 47 | 51 | ); 52 | } 53 | 54 | if (!inGitRepo && !accepted) { 55 | return ( 56 | 57 | 58 | 59 | ● OpenAI Codex{" "} 60 | 61 | (research preview) v{CLI_VERSION} 62 | 63 | 64 | 65 | 71 | 72 | Warning! It can be dangerous to run a 73 | coding agent outside of a git repo in case there are changes that 74 | you want to revert. Do you want to continue? 75 | 76 | {cwd} 77 | { 80 | app.exit(); 81 | onExit(); 82 | // eslint-disable-next-line 83 | console.error( 84 | "Quitting! Run again to accept or from inside a git repo", 85 | ); 86 | }} 87 | onConfirm={() => setAccepted(true)} 88 | /> 89 | 90 | 91 | ); 92 | } 93 | 94 | return ( 95 | 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /codex-cli/src/cli-singlepass.tsx: -------------------------------------------------------------------------------- 1 | import type { AppConfig } from "./utils/config"; 2 | 3 | import { SinglePassApp } from "./components/singlepass-cli-app"; 4 | import { render } from "ink"; 5 | import React from "react"; 6 | 7 | export async function runSinglePass({ 8 | originalPrompt, 9 | config, 10 | rootPath, 11 | }: { 12 | originalPrompt?: string; 13 | config: AppConfig; 14 | rootPath: string; 15 | }): Promise { 16 | return new Promise((resolve) => { 17 | render( 18 | resolve()} 23 | />, 24 | ); 25 | }); 26 | } 27 | 28 | export default {}; 29 | -------------------------------------------------------------------------------- /codex-cli/src/components/approval-mode-overlay.tsx: -------------------------------------------------------------------------------- 1 | import TypeaheadOverlay from "./typeahead-overlay.js"; 2 | import { AutoApprovalMode } from "../utils/auto-approval-mode.js"; 3 | import { Text } from "ink"; 4 | import React from "react"; 5 | 6 | type Props = { 7 | currentMode: string; 8 | onSelect: (mode: string) => void; 9 | onExit: () => void; 10 | }; 11 | 12 | /** 13 | * Overlay to switch between the different automatic‑approval policies. 14 | * 15 | * The list of available modes is derived from the AutoApprovalMode enum so we 16 | * stay in sync with the core agent behaviour. It re‑uses the generic 17 | * TypeaheadOverlay component for the actual UI/UX. 18 | */ 19 | export default function ApprovalModeOverlay({ 20 | currentMode, 21 | onSelect, 22 | onExit, 23 | }: Props): JSX.Element { 24 | const items = React.useMemo( 25 | () => 26 | Object.values(AutoApprovalMode).map((m) => ({ 27 | label: m, 28 | value: m, 29 | })), 30 | [], 31 | ); 32 | 33 | return ( 34 | 38 | Current mode: {currentMode} 39 | 40 | } 41 | initialItems={items} 42 | currentValue={currentMode} 43 | onSelect={onSelect} 44 | onExit={onExit} 45 | /> 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /codex-cli/src/components/chat/message-history.tsx: -------------------------------------------------------------------------------- 1 | import type { TerminalHeaderProps } from "./terminal-header.js"; 2 | import type { GroupedResponseItem } from "./use-message-grouping.js"; 3 | import type { ChatCompletionMessageParam } from "openai/resources/chat/completions.mjs"; 4 | 5 | import TerminalChatResponseItem from "./terminal-chat-response-item.js"; 6 | import TerminalHeader from "./terminal-header.js"; 7 | import { Box, Static } from "ink"; 8 | import React from "react"; 9 | 10 | // A batch entry can either be a standalone response item or a grouped set of 11 | // items (e.g. auto‑approved tool‑call batches) that should be rendered 12 | // together. 13 | type BatchEntry = { 14 | item?: ChatCompletionMessageParam; 15 | group?: GroupedResponseItem; 16 | }; 17 | type MessageHistoryProps = { 18 | batch: Array; 19 | groupCounts: Record; 20 | items: Array; 21 | userMsgCount: number; 22 | confirmationPrompt: React.ReactNode; 23 | loading: boolean; 24 | headerProps: TerminalHeaderProps; 25 | }; 26 | 27 | const MessageHistory: React.FC = ({ 28 | batch, 29 | headerProps, 30 | }) => { 31 | const messages = batch.map(({ item }) => item!); 32 | 33 | return ( 34 | 35 | {/* 36 | * The Static component receives a mixed array of the literal string 37 | * "header" plus the streamed ResponseItem objects. After filtering out 38 | * the header entry we can safely treat the remaining values as 39 | * ResponseItem, however TypeScript cannot infer the refined type from 40 | * the runtime check and therefore reports property‑access errors. 41 | * 42 | * A short cast after the refinement keeps the implementation tidy while 43 | * preserving type‑safety. 44 | */} 45 | 46 | {(item, index) => { 47 | if (item === "header") { 48 | return ; 49 | } 50 | const message = item as ChatCompletionMessageParam; 51 | return ( 52 | 60 | 61 | 62 | ); 63 | }} 64 | 65 | 66 | ); 67 | }; 68 | 69 | export default React.memo(MessageHistory); 70 | -------------------------------------------------------------------------------- /codex-cli/src/components/chat/terminal-chat-past-rollout.tsx: -------------------------------------------------------------------------------- 1 | import type { TerminalChatSession } from "../../utils/session.js"; 2 | import type { ChatCompletionMessageParam } from "openai/resources/chat/completions.mjs"; 3 | 4 | import TerminalChatResponseItem from "./terminal-chat-response-item"; 5 | import { Box, Text } from "ink"; 6 | import React from "react"; 7 | 8 | export default function TerminalChatPastRollout({ 9 | session, 10 | items, 11 | }: { 12 | session: TerminalChatSession; 13 | items: Array; 14 | }): React.ReactElement { 15 | const { version, id: sessionId, model } = session; 16 | return ( 17 | 18 | 19 | 20 | ● OpenAI Codex{" "} 21 | 22 | (research preview) v{version} 23 | 24 | 25 | 26 | 33 | 34 | localhost{" "} 35 | · session:{" "} 36 | 37 | {sessionId} 38 | 39 | 40 | 41 | When / Who:{" "} 42 | 43 | {session.timestamp} / {session.user} 44 | 45 | 46 | 47 | model: {model} 48 | 49 | 50 | 51 | {React.useMemo( 52 | () => 53 | items.map((item, key) => ( 54 | 55 | )), 56 | [items], 57 | )} 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx: -------------------------------------------------------------------------------- 1 | import { parseApplyPatch } from "../../parse-apply-patch"; 2 | import { shortenPath } from "../../utils/short-path"; 3 | import chalk from "chalk"; 4 | import { Text } from "ink"; 5 | import React from "react"; 6 | 7 | export function TerminalChatToolCallCommand({ 8 | commandForDisplay, 9 | }: { 10 | commandForDisplay: string; 11 | }): React.ReactElement { 12 | // ------------------------------------------------------------------------- 13 | // Colorize diff output inside the command preview: we detect individual 14 | // lines that begin with '+' or '-' (excluding the typical diff headers like 15 | // '+++', '---', '++', '--') and apply green/red coloring. This mirrors 16 | // how Git shows diffs and makes the patch easier to review. 17 | // ------------------------------------------------------------------------- 18 | 19 | const colorizedCommand = commandForDisplay 20 | .split("\n") 21 | .map((line) => { 22 | if (line.startsWith("+") && !line.startsWith("++")) { 23 | return chalk.green(line); 24 | } 25 | if (line.startsWith("-") && !line.startsWith("--")) { 26 | return chalk.red(line); 27 | } 28 | return line; 29 | }) 30 | .join("\n"); 31 | 32 | return ( 33 | <> 34 | Shell Command 35 | 36 | $ {colorizedCommand} 37 | 38 | 39 | ); 40 | } 41 | 42 | export function TerminalChatToolCallApplyPatch({ 43 | commandForDisplay, 44 | patch, 45 | }: { 46 | commandForDisplay: string; 47 | patch: string; 48 | }): React.ReactElement { 49 | const ops = React.useMemo(() => parseApplyPatch(patch), [patch]); 50 | const firstOp = ops?.[0]; 51 | 52 | const title = React.useMemo(() => { 53 | if (!firstOp) { 54 | return ""; 55 | } 56 | return capitalize(firstOp.type); 57 | }, [firstOp]); 58 | 59 | const filePath = React.useMemo(() => { 60 | if (!firstOp) { 61 | return ""; 62 | } 63 | return shortenPath(firstOp.path || "."); 64 | }, [firstOp]); 65 | 66 | if (ops == null) { 67 | return ( 68 | <> 69 | 70 | Invalid Patch 71 | 72 | 73 | The provided patch command is invalid. 74 | 75 | {commandForDisplay} 76 | 77 | ); 78 | } 79 | 80 | if (!firstOp) { 81 | return ( 82 | <> 83 | 84 | Empty Patch 85 | 86 | 87 | No operations found in the patch command. 88 | 89 | {commandForDisplay} 90 | 91 | ); 92 | } 93 | 94 | return ( 95 | <> 96 | 97 | {title} {filePath} 98 | 99 | 100 | $ {commandForDisplay} 101 | 102 | 103 | ); 104 | } 105 | 106 | const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); 107 | -------------------------------------------------------------------------------- /codex-cli/src/components/chat/terminal-header.tsx: -------------------------------------------------------------------------------- 1 | import type { AgentLoop } from "../../utils/agent/agent-loop.js"; 2 | 3 | import { Box, Text } from "ink"; 4 | import path from "node:path"; 5 | import React from "react"; 6 | 7 | export interface TerminalHeaderProps { 8 | terminalRows: number; 9 | version: string; 10 | PWD: string; 11 | model: string; 12 | approvalPolicy: string; 13 | colorsByPolicy: Record; 14 | agent?: AgentLoop; 15 | initialImagePaths?: Array; 16 | } 17 | 18 | const TerminalHeader: React.FC = ({ 19 | terminalRows, 20 | version, 21 | PWD, 22 | model, 23 | approvalPolicy, 24 | colorsByPolicy, 25 | agent, 26 | initialImagePaths, 27 | }) => { 28 | return ( 29 | <> 30 | {terminalRows < 10 ? ( 31 | // Compact header for small terminal windows 32 | 33 | ● Codex v{version} – {PWD} – {model} –{" "} 34 | {approvalPolicy} 35 | 36 | ) : ( 37 | <> 38 | 39 | 40 | ● OpenAI Codex{" "} 41 | 42 | (research preview) v{version} 43 | 44 | 45 | 46 | 53 | 54 | localhost session:{" "} 55 | 56 | {agent?.sessionId ?? ""} 57 | 58 | 59 | 60 | workdir: {PWD} 61 | 62 | 63 | model: {model} 64 | 65 | 66 | approval:{" "} 67 | 68 | {approvalPolicy} 69 | 70 | 71 | {initialImagePaths?.map((img, idx) => ( 72 | 73 | image:{" "} 74 | {path.basename(img)} 75 | 76 | ))} 77 | 78 | 79 | )} 80 | 81 | ); 82 | }; 83 | 84 | export default TerminalHeader; 85 | -------------------------------------------------------------------------------- /codex-cli/src/components/chat/terminal-message-history.tsx: -------------------------------------------------------------------------------- 1 | import type { TerminalHeaderProps } from "./terminal-header.js"; 2 | import type { GroupedResponseItem } from "./use-message-grouping.js"; 3 | import type { ChatCompletionMessageParam } from "openai/resources/chat/completions.mjs"; 4 | 5 | import TerminalChatResponseItem from "./terminal-chat-response-item.js"; 6 | import TerminalHeader from "./terminal-header.js"; 7 | import { Box, Static, Text } from "ink"; 8 | import React, { useMemo } from "react"; 9 | 10 | // A batch entry can either be a standalone response item or a grouped set of 11 | // items (e.g. auto‑approved tool‑call batches) that should be rendered 12 | // together. 13 | type BatchEntry = { 14 | item?: ChatCompletionMessageParam; 15 | group?: GroupedResponseItem; 16 | }; 17 | type MessageHistoryProps = { 18 | batch: Array; 19 | groupCounts: Record; 20 | items: Array; 21 | userMsgCount: number; 22 | confirmationPrompt: React.ReactNode; 23 | loading: boolean; 24 | thinkingSeconds: number; 25 | headerProps: TerminalHeaderProps; 26 | fullStdout: boolean; 27 | }; 28 | 29 | const MessageHistory: React.FC = ({ 30 | batch, 31 | headerProps, 32 | loading, 33 | thinkingSeconds, 34 | fullStdout, 35 | }) => { 36 | const [messages, debug] = useMemo( 37 | () => [batch.map(({ item }) => item!), process.env["DEBUG"]], 38 | [batch], 39 | ); 40 | 41 | return ( 42 | 43 | {loading && debug && ( 44 | 45 | {`(${thinkingSeconds}s)`} 46 | 47 | )} 48 | 49 | {(item, index) => { 50 | if (item === "header") { 51 | return ; 52 | } 53 | const message = item as ChatCompletionMessageParam; 54 | return ( 55 | 61 | 65 | 66 | ); 67 | }} 68 | 69 | 70 | ); 71 | }; 72 | 73 | export default React.memo(MessageHistory); 74 | -------------------------------------------------------------------------------- /codex-cli/src/components/chat/use-message-grouping.ts: -------------------------------------------------------------------------------- 1 | import type { ResponseItem } from "openai/resources/responses/responses.mjs"; 2 | 3 | /** 4 | * Represents a grouped sequence of response items (e.g., function call batches). 5 | */ 6 | export type GroupedResponseItem = { 7 | label: string; 8 | items: Array; 9 | }; 10 | -------------------------------------------------------------------------------- /codex-cli/src/components/help-overlay.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text, useInput } from "ink"; 2 | import React from "react"; 3 | 4 | /** 5 | * An overlay that lists the available slash‑commands and their description. 6 | * The overlay is purely informational and can be dismissed with the Escape 7 | * key. Keeping the implementation extremely small avoids adding any new 8 | * dependencies or complex state handling. 9 | */ 10 | export default function HelpOverlay({ 11 | onExit, 12 | }: { 13 | onExit: () => void; 14 | }): JSX.Element { 15 | useInput((input, key) => { 16 | if (key.escape || input === "q") { 17 | onExit(); 18 | } 19 | }); 20 | 21 | return ( 22 | 28 | 29 | Available commands 30 | 31 | 32 | 33 | 34 | Slash‑commands 35 | 36 | 37 | /help – show this help overlay 38 | 39 | 40 | /model – switch the LLM model in‑session 41 | 42 | 43 | /approval – switch auto‑approval mode 44 | 45 | 46 | /history – show command & file history 47 | for this session 48 | 49 | 50 | /clear – clear screen & context 51 | 52 | 53 | 54 | 55 | Keyboard shortcuts 56 | 57 | 58 | 59 | Enter – send message 60 | 61 | 62 | Ctrl+J – insert newline 63 | 64 | {/* Re-enable once we re-enable new input */} 65 | {/* 66 | 67 | Ctrl+X/Ctrl+E 68 |  – open external editor ($EDITOR) 69 | 70 | */} 71 | 72 | Up/Down – scroll prompt history 73 | 74 | 75 | 76 | Esc(✕2) 77 | {" "} 78 | – interrupt current action 79 | 80 | 81 | Ctrl+C – quit Codex 82 | 83 | 84 | 85 | 86 | esc or q to close 87 | 88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /codex-cli/src/components/model-overlay.tsx: -------------------------------------------------------------------------------- 1 | import type { AppConfig } from "src/utils/config.js"; 2 | 3 | import TypeaheadOverlay from "./typeahead-overlay.js"; 4 | import { 5 | getAvailableModels, 6 | RECOMMENDED_MODELS, 7 | } from "../utils/model-utils.js"; 8 | import { Box, Text, useInput } from "ink"; 9 | import React, { useEffect, useState } from "react"; 10 | 11 | /** 12 | * Props for . 13 | * 14 | * When `hasLastResponse` is true the user has already received at least one 15 | * assistant response in the current session which means switching models is no 16 | * longer supported – the overlay should therefore show an error and only allow 17 | * the user to close it. 18 | */ 19 | type Props = { 20 | currentModel: string; 21 | config: AppConfig; 22 | hasLastResponse: boolean; 23 | onSelect: (model: string) => void; 24 | onExit: () => void; 25 | }; 26 | 27 | export default function ModelOverlay({ 28 | currentModel, 29 | hasLastResponse, 30 | config, 31 | onSelect, 32 | onExit, 33 | }: Props): JSX.Element { 34 | const [items, setItems] = useState>( 35 | [], 36 | ); 37 | 38 | useEffect(() => { 39 | (async () => { 40 | const models = await getAvailableModels(config); 41 | 42 | // Split the list into recommended and “other” models. 43 | const recommended = RECOMMENDED_MODELS.filter((m) => models.includes(m)); 44 | const others = models.filter((m) => !recommended.includes(m)); 45 | 46 | const ordered = [...recommended, ...others.sort()]; 47 | 48 | setItems( 49 | ordered.map((m) => ({ 50 | label: recommended.includes(m) ? `⭐ ${m}` : m, 51 | value: m, 52 | })), 53 | ); 54 | })(); 55 | }, [config]); 56 | 57 | // --------------------------------------------------------------------------- 58 | // If the conversation already contains a response we cannot change the model 59 | // anymore because the backend requires a consistent model across the entire 60 | // run. In that scenario we replace the regular typeahead picker with a 61 | // simple message instructing the user to start a new chat. The only 62 | // available action is to dismiss the overlay (Esc or Enter). 63 | // --------------------------------------------------------------------------- 64 | 65 | // Always register input handling so hooks are called consistently. 66 | useInput((_input, key) => { 67 | if (hasLastResponse && (key.escape || key.return)) { 68 | onExit(); 69 | } 70 | }); 71 | 72 | if (hasLastResponse) { 73 | return ( 74 | 80 | 81 | 82 | Unable to switch model 83 | 84 | 85 | 86 | 87 | You can only pick a model before the assistant sends its first 88 | response. To use a different model please start a new chat. 89 | 90 | 91 | 92 | press esc or enter to close 93 | 94 | 95 | ); 96 | } 97 | 98 | return ( 99 | 103 | Current model: {currentModel} 104 | 105 | } 106 | initialItems={items} 107 | currentValue={currentModel} 108 | onSelect={onSelect} 109 | onExit={onExit} 110 | /> 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /codex-cli/src/components/onboarding/onboarding-approval-mode.tsx: -------------------------------------------------------------------------------- 1 | // @ts-expect-error select.js is JavaScript and has no types 2 | import { Select } from "../vendor/ink-select/select"; 3 | import { Box, Text } from "ink"; 4 | import React from "react"; 5 | import { AutoApprovalMode } from "src/utils/auto-approval-mode"; 6 | 7 | // TODO: figure out why `cli-spinners` fails on Node v20.9.0 8 | // which is why we have to do this in the first place 9 | 10 | export function OnboardingApprovalMode(): React.ReactElement { 11 | return ( 12 | 13 | Choose what you want to have to approve: 14 |