Spaces:
Paused
Paused
icebear0828 Claude Opus 4.6 commited on
Commit ·
22a7de1
1
Parent(s): 0d2f54c
feat: desktop context injection, multi-turn sessions, TLS impersonation & auto-update
Browse files- Inject Codex Desktop system prompt into every request for full feature parity
- Add previous_response_id for multi-turn conversation continuity
- Add curl-impersonate auto-detection with Chrome TLS fingerprint fallback
- Fix appcast XML parsing for element-style Sparkle tags
- Remove private maintenance scripts from public repo
- Add README with setup and usage instructions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- .gitignore +19 -0
- README.md +167 -0
- config/default.yaml +8 -2
- config/extraction-patterns.yaml +0 -74
- package.json +5 -2
- public/dashboard.html +8 -5
- public/login.html +0 -527
- scripts/apply-update.ts +0 -354
- scripts/check-update.ts +0 -157
- scripts/extract-fingerprint.ts +0 -445
- scripts/test-create-env-gh.ts +0 -74
- scripts/test-create-env-real.ts +0 -128
- scripts/test-env-full.ts +0 -113
- scripts/test-repo-format.ts +0 -27
- scripts/test-repo2.ts +0 -23
- scripts/test-responses-api.ts +0 -99
- scripts/test-snapshot.ts +0 -117
- src/auth/oauth-pkce.ts +29 -31
- src/config.ts +4 -0
- src/proxy/codex-api.ts +12 -10
- src/routes/chat.ts +23 -1
- src/session/manager.ts +10 -0
- src/tls/curl-binary.ts +151 -0
- src/tls/curl-fetch.ts +89 -0
- src/translation/codex-to-openai.ts +17 -4
- src/translation/openai-to-codex.ts +25 -1
- src/update-checker.ts +11 -5
.gitignore
CHANGED
|
@@ -1,7 +1,26 @@
|
|
| 1 |
node_modules/
|
| 2 |
dist/
|
| 3 |
data/
|
|
|
|
|
|
|
| 4 |
.env
|
| 5 |
*.log
|
| 6 |
.asar-out/
|
|
|
|
| 7 |
.claude/settings.local.json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
node_modules/
|
| 2 |
dist/
|
| 3 |
data/
|
| 4 |
+
docs/
|
| 5 |
+
bin/
|
| 6 |
.env
|
| 7 |
*.log
|
| 8 |
.asar-out/
|
| 9 |
+
tmp/
|
| 10 |
.claude/settings.local.json
|
| 11 |
+
|
| 12 |
+
# Maintenance scripts (private — not for public repo)
|
| 13 |
+
scripts/extract-fingerprint.ts
|
| 14 |
+
scripts/full-update.ts
|
| 15 |
+
scripts/apply-update.ts
|
| 16 |
+
scripts/setup-curl.ts
|
| 17 |
+
scripts/check-update.ts
|
| 18 |
+
scripts/test-*.ts
|
| 19 |
+
scripts/cron-update.sh
|
| 20 |
+
config/extraction-patterns.yaml
|
| 21 |
+
|
| 22 |
+
# Design / internal files
|
| 23 |
+
stitch-designs/
|
| 24 |
+
stitch-screens/
|
| 25 |
+
CLAUDE.md
|
| 26 |
+
*.tar.gz
|
README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Codex Proxy
|
| 2 |
+
|
| 3 |
+
A reverse proxy that exposes the [Codex Desktop](https://openai.com/codex) API as an OpenAI-compatible `/v1/chat/completions` endpoint. Use any OpenAI-compatible client (Cursor, Continue, VS Code, etc.) with Codex models — for free.
|
| 4 |
+
|
| 5 |
+
## Architecture
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
OpenAI-compatible client
|
| 9 |
+
│
|
| 10 |
+
POST /v1/chat/completions
|
| 11 |
+
│
|
| 12 |
+
▼
|
| 13 |
+
┌─────────────┐ POST /backend-api/codex/responses
|
| 14 |
+
│ Codex Proxy │ ──────────────────────────────────────► chatgpt.com
|
| 15 |
+
│ :8080 │ ◄────────────────────────────────────── (SSE stream)
|
| 16 |
+
└─────────────┘
|
| 17 |
+
│
|
| 18 |
+
SSE chat.completion.chunk
|
| 19 |
+
│
|
| 20 |
+
▼
|
| 21 |
+
Client
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
The proxy translates OpenAI Chat Completions format to the Codex Responses API format, handles authentication (OAuth PKCE), multi-account rotation, and Cloudflare bypass via curl subprocess.
|
| 25 |
+
|
| 26 |
+
## Quick Start
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
# 1. Install dependencies
|
| 30 |
+
npm install
|
| 31 |
+
|
| 32 |
+
# 2. Start the proxy (dev mode with hot reload)
|
| 33 |
+
npm run dev
|
| 34 |
+
|
| 35 |
+
# 3. Open the dashboard and log in with your ChatGPT account
|
| 36 |
+
# http://localhost:8080
|
| 37 |
+
|
| 38 |
+
# 4. Test a chat completion
|
| 39 |
+
curl http://localhost:8080/v1/chat/completions \
|
| 40 |
+
-H "Content-Type: application/json" \
|
| 41 |
+
-d '{
|
| 42 |
+
"model": "codex",
|
| 43 |
+
"messages": [{"role": "user", "content": "Hello!"}],
|
| 44 |
+
"stream": true
|
| 45 |
+
}'
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## Features
|
| 49 |
+
|
| 50 |
+
- **OpenAI-compatible API** — drop-in replacement for `/v1/chat/completions` and `/v1/models`
|
| 51 |
+
- **OAuth PKCE login** — native browser-based login, no manual token copying
|
| 52 |
+
- **Multi-account rotation** — add multiple ChatGPT accounts with automatic load balancing (`least_used` or `round_robin`)
|
| 53 |
+
- **Auto token refresh** — JWT tokens are refreshed automatically before expiry
|
| 54 |
+
- **Cloudflare bypass** — all upstream requests use curl subprocess with native TLS
|
| 55 |
+
- **Quota monitoring** — real-time Codex usage/quota display per account
|
| 56 |
+
- **Web dashboard** — manage accounts, view usage, and monitor status at `http://localhost:8080`
|
| 57 |
+
- **Auto-update detection** — polls the Codex Desktop appcast for new versions
|
| 58 |
+
|
| 59 |
+
## Available Models
|
| 60 |
+
|
| 61 |
+
| Model ID | Alias | Description |
|
| 62 |
+
|----------|-------|-------------|
|
| 63 |
+
| `gpt-5.3-codex` | `codex` | Latest frontier agentic coding model (default) |
|
| 64 |
+
| `gpt-5.2-codex` | — | Previous generation coding model |
|
| 65 |
+
| `gpt-5.1-codex-max` | `codex-max` | Maximum capability coding model |
|
| 66 |
+
| `gpt-5.2` | — | General-purpose model |
|
| 67 |
+
| `gpt-5.1-codex-mini` | `codex-mini` | Lightweight, fast coding model |
|
| 68 |
+
|
| 69 |
+
## API Usage
|
| 70 |
+
|
| 71 |
+
### Chat Completions (streaming)
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
curl http://localhost:8080/v1/chat/completions \
|
| 75 |
+
-H "Content-Type: application/json" \
|
| 76 |
+
-d '{
|
| 77 |
+
"model": "codex",
|
| 78 |
+
"messages": [
|
| 79 |
+
{"role": "system", "content": "You are a helpful coding assistant."},
|
| 80 |
+
{"role": "user", "content": "Write a Python function to check if a number is prime."}
|
| 81 |
+
],
|
| 82 |
+
"stream": true
|
| 83 |
+
}'
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
### Chat Completions (non-streaming)
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
curl http://localhost:8080/v1/chat/completions \
|
| 90 |
+
-H "Content-Type: application/json" \
|
| 91 |
+
-d '{
|
| 92 |
+
"model": "codex",
|
| 93 |
+
"messages": [{"role": "user", "content": "Hello!"}],
|
| 94 |
+
"stream": false
|
| 95 |
+
}'
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
### List Models
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
curl http://localhost:8080/v1/models
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
### Check Account Quota
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
curl "http://localhost:8080/auth/accounts?quota=true"
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
## Configuration
|
| 111 |
+
|
| 112 |
+
All configuration is in `config/default.yaml`:
|
| 113 |
+
|
| 114 |
+
| Section | Key Settings |
|
| 115 |
+
|---------|-------------|
|
| 116 |
+
| `api` | `base_url`, `timeout_seconds` |
|
| 117 |
+
| `client` | `originator`, `app_version`, `platform`, `arch` |
|
| 118 |
+
| `model` | `default` model, `default_reasoning_effort` |
|
| 119 |
+
| `auth` | `oauth_client_id`, `rotation_strategy`, `rate_limit_backoff_seconds` |
|
| 120 |
+
| `server` | `host`, `port`, `proxy_api_key` |
|
| 121 |
+
|
| 122 |
+
Environment variable overrides:
|
| 123 |
+
|
| 124 |
+
| Variable | Overrides |
|
| 125 |
+
|----------|-----------|
|
| 126 |
+
| `PORT` | `server.port` |
|
| 127 |
+
| `CODEX_PLATFORM` | `client.platform` |
|
| 128 |
+
| `CODEX_ARCH` | `client.arch` |
|
| 129 |
+
| `CODEX_JWT_TOKEN` | `auth.jwt_token` |
|
| 130 |
+
|
| 131 |
+
## Client Setup Examples
|
| 132 |
+
|
| 133 |
+
### Cursor
|
| 134 |
+
|
| 135 |
+
Settings > Models > OpenAI API Base:
|
| 136 |
+
```
|
| 137 |
+
http://localhost:8080/v1
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
### Continue (VS Code)
|
| 141 |
+
|
| 142 |
+
`~/.continue/config.json`:
|
| 143 |
+
```json
|
| 144 |
+
{
|
| 145 |
+
"models": [{
|
| 146 |
+
"title": "Codex",
|
| 147 |
+
"provider": "openai",
|
| 148 |
+
"model": "codex",
|
| 149 |
+
"apiBase": "http://localhost:8080/v1"
|
| 150 |
+
}]
|
| 151 |
+
}
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
## Scripts
|
| 155 |
+
|
| 156 |
+
| Command | Description |
|
| 157 |
+
|---------|-------------|
|
| 158 |
+
| `npm run dev` | Start dev server with hot reload |
|
| 159 |
+
| `npm run build` | Compile TypeScript to `dist/` |
|
| 160 |
+
| `npm start` | Run compiled server |
|
| 161 |
+
| `npm run check-update` | Check for new Codex Desktop versions |
|
| 162 |
+
| `npm run extract -- --path <asar>` | Extract fingerprint from Codex app |
|
| 163 |
+
| `npm run apply-update` | Apply extracted fingerprint updates |
|
| 164 |
+
|
| 165 |
+
## License
|
| 166 |
+
|
| 167 |
+
For personal use only. This project is not affiliated with OpenAI.
|
config/default.yaml
CHANGED
|
@@ -4,8 +4,8 @@ api:
|
|
| 4 |
|
| 5 |
client:
|
| 6 |
originator: "Codex Desktop"
|
| 7 |
-
app_version: "
|
| 8 |
-
build_number: "
|
| 9 |
platform: "darwin"
|
| 10 |
arch: "arm64"
|
| 11 |
|
|
@@ -36,6 +36,12 @@ session:
|
|
| 36 |
ttl_minutes: 60
|
| 37 |
cleanup_interval_minutes: 5
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
streaming:
|
| 40 |
status_as_content: false
|
| 41 |
chunk_size: 100
|
|
|
|
| 4 |
|
| 5 |
client:
|
| 6 |
originator: "Codex Desktop"
|
| 7 |
+
app_version: "26.217.1959"
|
| 8 |
+
build_number: "669"
|
| 9 |
platform: "darwin"
|
| 10 |
arch: "arm64"
|
| 11 |
|
|
|
|
| 36 |
ttl_minutes: 60
|
| 37 |
cleanup_interval_minutes: 5
|
| 38 |
|
| 39 |
+
tls:
|
| 40 |
+
# curl binary path. "auto" = detect bin/curl-impersonate-chrome, fallback to system curl
|
| 41 |
+
curl_binary: "auto"
|
| 42 |
+
# Chrome profile for --impersonate flag (auto-detected when curl-impersonate supports it)
|
| 43 |
+
impersonate_profile: "chrome136"
|
| 44 |
+
|
| 45 |
streaming:
|
| 46 |
status_as_content: false
|
| 47 |
chunk_size: 100
|
config/extraction-patterns.yaml
DELETED
|
@@ -1,74 +0,0 @@
|
|
| 1 |
-
# Regex patterns for extracting values from Codex Desktop's main.js
|
| 2 |
-
# These patterns are applied to the beautified (js-beautify) output.
|
| 3 |
-
# Update these when the minified variable names change across versions.
|
| 4 |
-
#
|
| 5 |
-
# NOTE: Use single-quoted YAML strings for regex patterns. In YAML single quotes,
|
| 6 |
-
# backslashes are literal (no escaping needed). Only '' is special (escaped single quote).
|
| 7 |
-
|
| 8 |
-
# Version info from package.json (no regex needed — direct JSON parse)
|
| 9 |
-
package_json:
|
| 10 |
-
version_key: "version"
|
| 11 |
-
build_number_key: "codexBuildNumber"
|
| 12 |
-
sparkle_feed_key: "codexSparkleFeedUrl"
|
| 13 |
-
|
| 14 |
-
# Patterns for main.js content extraction
|
| 15 |
-
main_js:
|
| 16 |
-
# API base URL
|
| 17 |
-
api_base_url:
|
| 18 |
-
pattern: 'https://[^"]+/backend-api'
|
| 19 |
-
description: "WHAM API base URL"
|
| 20 |
-
|
| 21 |
-
# Originator header value — in beautified code: variable = "Codex Desktop";
|
| 22 |
-
originator:
|
| 23 |
-
pattern: '= "(Codex [^"]+)"'
|
| 24 |
-
group: 1
|
| 25 |
-
description: "originator header sent with requests"
|
| 26 |
-
|
| 27 |
-
# Model IDs — match quoted gpt-X.Y(-codex)(-variant) strings
|
| 28 |
-
# In beautified code: = "gpt-5.3-codex", LF = "gpt-5.1-codex-mini", etc.
|
| 29 |
-
models:
|
| 30 |
-
pattern: '"(gpt-[0-9]+[.][0-9]+(?:-codex)?(?:-[a-z]+)?)"'
|
| 31 |
-
group: 1
|
| 32 |
-
global: true
|
| 33 |
-
description: "All model ID strings"
|
| 34 |
-
|
| 35 |
-
# WHAM API endpoint paths
|
| 36 |
-
wham_endpoints:
|
| 37 |
-
pattern: '"(/wham/[^"]+)"'
|
| 38 |
-
group: 1
|
| 39 |
-
global: true
|
| 40 |
-
description: "WHAM API endpoint paths"
|
| 41 |
-
|
| 42 |
-
# User-Agent template
|
| 43 |
-
user_agent:
|
| 44 |
-
pattern: "Codex Desktop/"
|
| 45 |
-
description: "User-Agent prefix check"
|
| 46 |
-
|
| 47 |
-
# Desktop context system prompt (variable oF)
|
| 48 |
-
desktop_context:
|
| 49 |
-
start_marker: "# Codex desktop context"
|
| 50 |
-
end_marker: "</app-context>"
|
| 51 |
-
description: "Full desktop context system prompt"
|
| 52 |
-
|
| 53 |
-
# Title generation prompt (function KZ)
|
| 54 |
-
title_generation:
|
| 55 |
-
start_marker: "You are a helpful assistant. You will be presented with a user prompt"
|
| 56 |
-
end_pattern: '.join('
|
| 57 |
-
description: "Task title generation system prompt"
|
| 58 |
-
|
| 59 |
-
# PR generation prompt (function n9)
|
| 60 |
-
pr_generation:
|
| 61 |
-
start_marker: "You are a helpful assistant. Generate a pull request title"
|
| 62 |
-
end_pattern: '.join('
|
| 63 |
-
description: "PR title/body generation system prompt"
|
| 64 |
-
|
| 65 |
-
# Automation response template (constant qK)
|
| 66 |
-
automation_response:
|
| 67 |
-
start_marker: "Response MUST end with a remark-directive block"
|
| 68 |
-
end_pattern: "^`;$"
|
| 69 |
-
description: "Automation response template with directive rules"
|
| 70 |
-
|
| 71 |
-
# Info.plist keys (macOS only)
|
| 72 |
-
info_plist:
|
| 73 |
-
version_key: "CFBundleShortVersionString"
|
| 74 |
-
build_key: "CFBundleVersion"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"name": "codex-proxy",
|
| 3 |
"version": "1.0.0",
|
| 4 |
-
"description": "Reverse proxy that exposes Codex Desktop
|
| 5 |
"type": "module",
|
| 6 |
"scripts": {
|
| 7 |
"dev": "tsx watch src/index.ts",
|
|
@@ -11,7 +11,10 @@
|
|
| 11 |
"check-update:watch": "tsx scripts/check-update.ts --watch",
|
| 12 |
"extract": "tsx scripts/extract-fingerprint.ts",
|
| 13 |
"apply-update": "tsx scripts/apply-update.ts",
|
| 14 |
-
"apply-update:dry": "tsx scripts/apply-update.ts --dry-run"
|
|
|
|
|
|
|
|
|
|
| 15 |
},
|
| 16 |
"dependencies": {
|
| 17 |
"hono": "^4.0.0",
|
|
|
|
| 1 |
{
|
| 2 |
"name": "codex-proxy",
|
| 3 |
"version": "1.0.0",
|
| 4 |
+
"description": "Reverse proxy that exposes Codex Desktop Responses API as OpenAI-compatible /v1/chat/completions",
|
| 5 |
"type": "module",
|
| 6 |
"scripts": {
|
| 7 |
"dev": "tsx watch src/index.ts",
|
|
|
|
| 11 |
"check-update:watch": "tsx scripts/check-update.ts --watch",
|
| 12 |
"extract": "tsx scripts/extract-fingerprint.ts",
|
| 13 |
"apply-update": "tsx scripts/apply-update.ts",
|
| 14 |
+
"apply-update:dry": "tsx scripts/apply-update.ts --dry-run",
|
| 15 |
+
"setup": "tsx scripts/setup-curl.ts",
|
| 16 |
+
"update": "tsx scripts/full-update.ts",
|
| 17 |
+
"postinstall": "tsx scripts/setup-curl.ts"
|
| 18 |
},
|
| 19 |
"dependencies": {
|
| 20 |
"hono": "^4.0.0",
|
public/dashboard.html
CHANGED
|
@@ -808,15 +808,18 @@
|
|
| 808 |
const model = escapeHtml(document.getElementById('defaultModel').value);
|
| 809 |
|
| 810 |
document.getElementById('code-python').innerHTML =
|
| 811 |
-
`
|
| 812 |
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
|
|
|
|
|
|
|
|
|
| 816 |
model="${model}",
|
| 817 |
messages=[
|
| 818 |
{"role": "user", "content": "Explain quantum computing in simple terms"}
|
| 819 |
-
]
|
| 820 |
)
|
| 821 |
|
| 822 |
print(response.choices[0].message.content)
|
|
|
|
| 808 |
const model = escapeHtml(document.getElementById('defaultModel').value);
|
| 809 |
|
| 810 |
document.getElementById('code-python').innerHTML =
|
| 811 |
+
`from openai import OpenAI
|
| 812 |
|
| 813 |
+
client = OpenAI(
|
| 814 |
+
base_url="${baseUrl}",
|
| 815 |
+
api_key="${apiKey}",
|
| 816 |
+
)
|
| 817 |
+
|
| 818 |
+
response = client.chat.completions.create(
|
| 819 |
model="${model}",
|
| 820 |
messages=[
|
| 821 |
{"role": "user", "content": "Explain quantum computing in simple terms"}
|
| 822 |
+
],
|
| 823 |
)
|
| 824 |
|
| 825 |
print(response.choices[0].message.content)
|
public/login.html
DELETED
|
@@ -1,527 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>Codex Proxy - Login</title>
|
| 7 |
-
<style>
|
| 8 |
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
-
body {
|
| 10 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 11 |
-
background: #0d1117;
|
| 12 |
-
color: #c9d1d9;
|
| 13 |
-
min-height: 100vh;
|
| 14 |
-
display: flex;
|
| 15 |
-
align-items: center;
|
| 16 |
-
justify-content: center;
|
| 17 |
-
}
|
| 18 |
-
.container {
|
| 19 |
-
max-width: 420px;
|
| 20 |
-
width: 100%;
|
| 21 |
-
padding: 2rem;
|
| 22 |
-
}
|
| 23 |
-
.logo {
|
| 24 |
-
text-align: center;
|
| 25 |
-
margin-bottom: 2rem;
|
| 26 |
-
}
|
| 27 |
-
.logo h1 {
|
| 28 |
-
font-size: 1.5rem;
|
| 29 |
-
color: #58a6ff;
|
| 30 |
-
font-weight: 600;
|
| 31 |
-
}
|
| 32 |
-
.logo p {
|
| 33 |
-
color: #8b949e;
|
| 34 |
-
margin-top: 0.5rem;
|
| 35 |
-
font-size: 0.9rem;
|
| 36 |
-
}
|
| 37 |
-
.card {
|
| 38 |
-
background: #161b22;
|
| 39 |
-
border: 1px solid #30363d;
|
| 40 |
-
border-radius: 12px;
|
| 41 |
-
padding: 2rem;
|
| 42 |
-
}
|
| 43 |
-
.btn {
|
| 44 |
-
display: block;
|
| 45 |
-
width: 100%;
|
| 46 |
-
padding: 12px 16px;
|
| 47 |
-
border: none;
|
| 48 |
-
border-radius: 8px;
|
| 49 |
-
font-size: 1rem;
|
| 50 |
-
font-weight: 500;
|
| 51 |
-
cursor: pointer;
|
| 52 |
-
text-align: center;
|
| 53 |
-
text-decoration: none;
|
| 54 |
-
transition: background 0.2s;
|
| 55 |
-
}
|
| 56 |
-
.btn-primary {
|
| 57 |
-
background: #238636;
|
| 58 |
-
color: #fff;
|
| 59 |
-
}
|
| 60 |
-
.btn-primary:hover {
|
| 61 |
-
background: #2ea043;
|
| 62 |
-
}
|
| 63 |
-
.btn-primary:disabled {
|
| 64 |
-
opacity: 0.5;
|
| 65 |
-
cursor: not-allowed;
|
| 66 |
-
}
|
| 67 |
-
.divider {
|
| 68 |
-
text-align: center;
|
| 69 |
-
margin: 1.5rem 0;
|
| 70 |
-
color: #484f58;
|
| 71 |
-
font-size: 0.85rem;
|
| 72 |
-
}
|
| 73 |
-
.divider::before, .divider::after {
|
| 74 |
-
content: '';
|
| 75 |
-
display: inline-block;
|
| 76 |
-
width: 30%;
|
| 77 |
-
height: 1px;
|
| 78 |
-
background: #30363d;
|
| 79 |
-
vertical-align: middle;
|
| 80 |
-
margin: 0 0.5rem;
|
| 81 |
-
}
|
| 82 |
-
.input-group {
|
| 83 |
-
margin-bottom: 1rem;
|
| 84 |
-
}
|
| 85 |
-
.input-group label {
|
| 86 |
-
display: block;
|
| 87 |
-
font-size: 0.85rem;
|
| 88 |
-
color: #8b949e;
|
| 89 |
-
margin-bottom: 0.5rem;
|
| 90 |
-
}
|
| 91 |
-
.input-group textarea {
|
| 92 |
-
width: 100%;
|
| 93 |
-
padding: 10px;
|
| 94 |
-
background: #0d1117;
|
| 95 |
-
border: 1px solid #30363d;
|
| 96 |
-
border-radius: 6px;
|
| 97 |
-
color: #c9d1d9;
|
| 98 |
-
font-family: monospace;
|
| 99 |
-
font-size: 0.85rem;
|
| 100 |
-
resize: vertical;
|
| 101 |
-
min-height: 80px;
|
| 102 |
-
}
|
| 103 |
-
.input-group textarea:focus {
|
| 104 |
-
outline: none;
|
| 105 |
-
border-color: #58a6ff;
|
| 106 |
-
}
|
| 107 |
-
.error {
|
| 108 |
-
color: #f85149;
|
| 109 |
-
font-size: 0.85rem;
|
| 110 |
-
margin-top: 0.5rem;
|
| 111 |
-
display: none;
|
| 112 |
-
}
|
| 113 |
-
.success {
|
| 114 |
-
color: #3fb950;
|
| 115 |
-
font-size: 0.85rem;
|
| 116 |
-
margin-top: 0.5rem;
|
| 117 |
-
display: none;
|
| 118 |
-
}
|
| 119 |
-
.help {
|
| 120 |
-
margin-top: 1rem;
|
| 121 |
-
font-size: 0.8rem;
|
| 122 |
-
color: #484f58;
|
| 123 |
-
line-height: 1.5;
|
| 124 |
-
}
|
| 125 |
-
#pasteSection {
|
| 126 |
-
display: none;
|
| 127 |
-
}
|
| 128 |
-
.paste-instructions {
|
| 129 |
-
background: #0d1117;
|
| 130 |
-
border: 1px solid #30363d;
|
| 131 |
-
border-radius: 6px;
|
| 132 |
-
padding: 0.75rem;
|
| 133 |
-
margin-bottom: 1rem;
|
| 134 |
-
font-size: 0.82rem;
|
| 135 |
-
line-height: 1.5;
|
| 136 |
-
color: #8b949e;
|
| 137 |
-
}
|
| 138 |
-
.paste-instructions strong {
|
| 139 |
-
color: #c9d1d9;
|
| 140 |
-
}
|
| 141 |
-
.paste-instructions ol {
|
| 142 |
-
margin: 0.5rem 0 0 1.2rem;
|
| 143 |
-
}
|
| 144 |
-
.btn-secondary {
|
| 145 |
-
display: block;
|
| 146 |
-
width: 100%;
|
| 147 |
-
padding: 12px 16px;
|
| 148 |
-
border: 1px solid #30363d;
|
| 149 |
-
border-radius: 8px;
|
| 150 |
-
background: #21262d;
|
| 151 |
-
color: #c9d1d9;
|
| 152 |
-
font-size: 1rem;
|
| 153 |
-
font-weight: 500;
|
| 154 |
-
cursor: pointer;
|
| 155 |
-
text-align: center;
|
| 156 |
-
transition: background 0.2s;
|
| 157 |
-
}
|
| 158 |
-
.btn-secondary:hover { background: #30363d; }
|
| 159 |
-
.btn-secondary:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 160 |
-
.device-code-box {
|
| 161 |
-
background: #0d1117;
|
| 162 |
-
border: 1px solid #30363d;
|
| 163 |
-
border-radius: 8px;
|
| 164 |
-
padding: 1.5rem;
|
| 165 |
-
text-align: center;
|
| 166 |
-
margin-top: 1rem;
|
| 167 |
-
display: none;
|
| 168 |
-
}
|
| 169 |
-
.device-code-box .user-code {
|
| 170 |
-
font-family: monospace;
|
| 171 |
-
font-size: 2rem;
|
| 172 |
-
font-weight: 700;
|
| 173 |
-
color: #58a6ff;
|
| 174 |
-
letter-spacing: 0.15em;
|
| 175 |
-
margin: 0.75rem 0;
|
| 176 |
-
}
|
| 177 |
-
.device-code-box .verify-link {
|
| 178 |
-
color: #3fb950;
|
| 179 |
-
text-decoration: none;
|
| 180 |
-
font-size: 0.9rem;
|
| 181 |
-
}
|
| 182 |
-
.device-code-box .verify-link:hover { text-decoration: underline; }
|
| 183 |
-
.device-code-box .status-text {
|
| 184 |
-
color: #8b949e;
|
| 185 |
-
font-size: 0.82rem;
|
| 186 |
-
margin-top: 0.75rem;
|
| 187 |
-
}
|
| 188 |
-
.spinner {
|
| 189 |
-
display: inline-block;
|
| 190 |
-
width: 14px;
|
| 191 |
-
height: 14px;
|
| 192 |
-
border: 2px solid rgba(255,255,255,0.3);
|
| 193 |
-
border-top-color: #fff;
|
| 194 |
-
border-radius: 50%;
|
| 195 |
-
animation: spin 0.8s linear infinite;
|
| 196 |
-
vertical-align: middle;
|
| 197 |
-
margin-right: 6px;
|
| 198 |
-
}
|
| 199 |
-
@keyframes spin { to { transform: rotate(360deg); } }
|
| 200 |
-
</style>
|
| 201 |
-
</head>
|
| 202 |
-
<body>
|
| 203 |
-
<div class="container">
|
| 204 |
-
<div class="logo">
|
| 205 |
-
<h1>Codex Proxy</h1>
|
| 206 |
-
<p>OpenAI-compatible API for Codex Desktop</p>
|
| 207 |
-
</div>
|
| 208 |
-
<div class="card">
|
| 209 |
-
<button class="btn btn-primary" id="loginBtn" onclick="startLogin()">
|
| 210 |
-
Login with ChatGPT
|
| 211 |
-
</button>
|
| 212 |
-
|
| 213 |
-
<div id="pasteSection">
|
| 214 |
-
<div class="paste-instructions">
|
| 215 |
-
<strong>Remote login:</strong> If the popup shows an error or you're on a different machine:
|
| 216 |
-
<ol>
|
| 217 |
-
<li>Complete login in the popup</li>
|
| 218 |
-
<li>Copy the full URL from the popup's address bar<br>(starts with <code>http://localhost:...</code>)</li>
|
| 219 |
-
<li>Paste it below and click Submit</li>
|
| 220 |
-
</ol>
|
| 221 |
-
</div>
|
| 222 |
-
<div class="input-group">
|
| 223 |
-
<label>Paste the callback URL here</label>
|
| 224 |
-
<textarea id="callbackInput" placeholder="http://localhost:54321/auth/callback?code=...&state=..."></textarea>
|
| 225 |
-
</div>
|
| 226 |
-
<button class="btn btn-primary" id="relayBtn" onclick="submitRelay()">
|
| 227 |
-
Submit Callback URL
|
| 228 |
-
</button>
|
| 229 |
-
<div class="error" id="relayError"></div>
|
| 230 |
-
<div class="success" id="relaySuccess"></div>
|
| 231 |
-
</div>
|
| 232 |
-
|
| 233 |
-
<div class="divider">or</div>
|
| 234 |
-
|
| 235 |
-
<button class="btn btn-primary" id="deviceCodeBtn" onclick="startDeviceCode()">
|
| 236 |
-
Sign in with Device Code
|
| 237 |
-
</button>
|
| 238 |
-
<div class="device-code-box" id="deviceCodeBox">
|
| 239 |
-
<div style="color:#8b949e;font-size:0.85rem">Enter this code at:</div>
|
| 240 |
-
<a class="verify-link" id="deviceVerifyLink" href="#" target="_blank" rel="noopener"></a>
|
| 241 |
-
<div class="user-code" id="deviceUserCode"></div>
|
| 242 |
-
<div class="status-text" id="deviceStatus"><span class="spinner"></span> Waiting for authorization...</div>
|
| 243 |
-
<div class="error" id="deviceError"></div>
|
| 244 |
-
<div class="success" id="deviceSuccess"></div>
|
| 245 |
-
</div>
|
| 246 |
-
|
| 247 |
-
<div style="margin-top:0.75rem">
|
| 248 |
-
<button class="btn-secondary" id="cliImportBtn" onclick="importCli()">
|
| 249 |
-
Import CLI Token (~/.codex/auth.json)
|
| 250 |
-
</button>
|
| 251 |
-
<div class="error" id="cliError"></div>
|
| 252 |
-
<div class="success" id="cliSuccess"></div>
|
| 253 |
-
</div>
|
| 254 |
-
|
| 255 |
-
<div class="divider">or</div>
|
| 256 |
-
|
| 257 |
-
<div id="manualSection">
|
| 258 |
-
<div class="input-group">
|
| 259 |
-
<label>Paste your ChatGPT access token</label>
|
| 260 |
-
<textarea id="tokenInput" placeholder="eyJhbGciOiJSUzI1NiIs..."></textarea>
|
| 261 |
-
</div>
|
| 262 |
-
<button class="btn btn-primary" id="submitToken" onclick="submitToken()">
|
| 263 |
-
Submit Token
|
| 264 |
-
</button>
|
| 265 |
-
<div class="error" id="errorMsg"></div>
|
| 266 |
-
<div class="success" id="successMsg"></div>
|
| 267 |
-
<div class="help">
|
| 268 |
-
To get your token: Open ChatGPT in browser → DevTools (F12) →
|
| 269 |
-
Application → Cookies → find <code>__Secure-next-auth.session-token</code>
|
| 270 |
-
or check Network tab for <code>Authorization: Bearer ...</code> header.
|
| 271 |
-
</div>
|
| 272 |
-
</div>
|
| 273 |
-
</div>
|
| 274 |
-
</div>
|
| 275 |
-
|
| 276 |
-
<script>
|
| 277 |
-
let pollTimer = null;
|
| 278 |
-
|
| 279 |
-
// Listen for postMessage from OAuth callback popup (cross-port communication)
|
| 280 |
-
window.addEventListener('message', (event) => {
|
| 281 |
-
if (event.data?.type === 'oauth-callback-success') {
|
| 282 |
-
if (pollTimer) clearInterval(pollTimer);
|
| 283 |
-
window.location.href = '/';
|
| 284 |
-
}
|
| 285 |
-
});
|
| 286 |
-
|
| 287 |
-
async function startLogin() {
|
| 288 |
-
const btn = document.getElementById('loginBtn');
|
| 289 |
-
btn.disabled = true;
|
| 290 |
-
btn.innerHTML = '<span class="spinner"></span> Opening login...';
|
| 291 |
-
|
| 292 |
-
try {
|
| 293 |
-
const resp = await fetch('/auth/login-start', { method: 'POST' });
|
| 294 |
-
const data = await resp.json();
|
| 295 |
-
|
| 296 |
-
if (!resp.ok || !data.authUrl) {
|
| 297 |
-
throw new Error(data.error || 'Failed to start login');
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
// Open Auth0 in popup
|
| 301 |
-
const popup = window.open(data.authUrl, 'oauth_login', 'width=600,height=700,scrollbars=yes');
|
| 302 |
-
|
| 303 |
-
// Show paste section
|
| 304 |
-
document.getElementById('pasteSection').style.display = 'block';
|
| 305 |
-
btn.innerHTML = 'Login with ChatGPT';
|
| 306 |
-
btn.disabled = false;
|
| 307 |
-
|
| 308 |
-
// Poll auth status — if callback server handles it, we auto-redirect
|
| 309 |
-
startPolling();
|
| 310 |
-
|
| 311 |
-
} catch (err) {
|
| 312 |
-
btn.innerHTML = 'Login with ChatGPT';
|
| 313 |
-
btn.disabled = false;
|
| 314 |
-
const errEl = document.getElementById('relayError');
|
| 315 |
-
errEl.textContent = err.message;
|
| 316 |
-
errEl.style.display = 'block';
|
| 317 |
-
document.getElementById('pasteSection').style.display = 'block';
|
| 318 |
-
}
|
| 319 |
-
}
|
| 320 |
-
|
| 321 |
-
function startPolling() {
|
| 322 |
-
if (pollTimer) clearInterval(pollTimer);
|
| 323 |
-
pollTimer = setInterval(async () => {
|
| 324 |
-
try {
|
| 325 |
-
const resp = await fetch('/auth/status');
|
| 326 |
-
const data = await resp.json();
|
| 327 |
-
if (data.authenticated) {
|
| 328 |
-
clearInterval(pollTimer);
|
| 329 |
-
window.location.href = '/';
|
| 330 |
-
}
|
| 331 |
-
} catch {}
|
| 332 |
-
}, 2000);
|
| 333 |
-
|
| 334 |
-
// Stop polling after 5 minutes
|
| 335 |
-
setTimeout(() => {
|
| 336 |
-
if (pollTimer) clearInterval(pollTimer);
|
| 337 |
-
}, 5 * 60 * 1000);
|
| 338 |
-
}
|
| 339 |
-
|
| 340 |
-
async function submitRelay() {
|
| 341 |
-
const callbackUrl = document.getElementById('callbackInput').value.trim();
|
| 342 |
-
const errEl = document.getElementById('relayError');
|
| 343 |
-
const successEl = document.getElementById('relaySuccess');
|
| 344 |
-
errEl.style.display = 'none';
|
| 345 |
-
successEl.style.display = 'none';
|
| 346 |
-
|
| 347 |
-
if (!callbackUrl) {
|
| 348 |
-
errEl.textContent = 'Please paste the callback URL';
|
| 349 |
-
errEl.style.display = 'block';
|
| 350 |
-
return;
|
| 351 |
-
}
|
| 352 |
-
|
| 353 |
-
const btn = document.getElementById('relayBtn');
|
| 354 |
-
btn.disabled = true;
|
| 355 |
-
btn.innerHTML = '<span class="spinner"></span> Exchanging...';
|
| 356 |
-
|
| 357 |
-
try {
|
| 358 |
-
const resp = await fetch('/auth/code-relay', {
|
| 359 |
-
method: 'POST',
|
| 360 |
-
headers: { 'Content-Type': 'application/json' },
|
| 361 |
-
body: JSON.stringify({ callbackUrl }),
|
| 362 |
-
});
|
| 363 |
-
const data = await resp.json();
|
| 364 |
-
|
| 365 |
-
if (resp.ok && data.success) {
|
| 366 |
-
successEl.textContent = 'Login successful! Redirecting...';
|
| 367 |
-
successEl.style.display = 'block';
|
| 368 |
-
if (pollTimer) clearInterval(pollTimer);
|
| 369 |
-
setTimeout(() => window.location.href = '/', 1000);
|
| 370 |
-
} else {
|
| 371 |
-
errEl.textContent = data.error || 'Failed to exchange code';
|
| 372 |
-
errEl.style.display = 'block';
|
| 373 |
-
}
|
| 374 |
-
} catch (err) {
|
| 375 |
-
errEl.textContent = 'Network error: ' + err.message;
|
| 376 |
-
errEl.style.display = 'block';
|
| 377 |
-
} finally {
|
| 378 |
-
btn.innerHTML = 'Submit Callback URL';
|
| 379 |
-
btn.disabled = false;
|
| 380 |
-
}
|
| 381 |
-
}
|
| 382 |
-
|
| 383 |
-
// ── Device Code Flow ──────────────────────────────
|
| 384 |
-
let devicePollTimer = null;
|
| 385 |
-
|
| 386 |
-
async function startDeviceCode() {
|
| 387 |
-
const btn = document.getElementById('deviceCodeBtn');
|
| 388 |
-
const box = document.getElementById('deviceCodeBox');
|
| 389 |
-
const errEl = document.getElementById('deviceError');
|
| 390 |
-
const successEl = document.getElementById('deviceSuccess');
|
| 391 |
-
const statusEl = document.getElementById('deviceStatus');
|
| 392 |
-
errEl.style.display = 'none';
|
| 393 |
-
successEl.style.display = 'none';
|
| 394 |
-
|
| 395 |
-
btn.disabled = true;
|
| 396 |
-
btn.innerHTML = '<span class="spinner"></span> Requesting code...';
|
| 397 |
-
|
| 398 |
-
try {
|
| 399 |
-
const resp = await fetch('/auth/device-login', { method: 'POST' });
|
| 400 |
-
const data = await resp.json();
|
| 401 |
-
|
| 402 |
-
if (!resp.ok || !data.userCode) {
|
| 403 |
-
throw new Error(data.error || 'Failed to request device code');
|
| 404 |
-
}
|
| 405 |
-
|
| 406 |
-
// Show the code box
|
| 407 |
-
box.style.display = 'block';
|
| 408 |
-
document.getElementById('deviceUserCode').textContent = data.userCode;
|
| 409 |
-
const link = document.getElementById('deviceVerifyLink');
|
| 410 |
-
link.href = data.verificationUriComplete || data.verificationUri;
|
| 411 |
-
link.textContent = data.verificationUri;
|
| 412 |
-
statusEl.innerHTML = '<span class="spinner"></span> Waiting for authorization...';
|
| 413 |
-
|
| 414 |
-
btn.innerHTML = 'Sign in with Device Code';
|
| 415 |
-
btn.disabled = false;
|
| 416 |
-
|
| 417 |
-
// Start polling
|
| 418 |
-
const interval = (data.interval || 5) * 1000;
|
| 419 |
-
const deviceCode = data.deviceCode;
|
| 420 |
-
if (devicePollTimer) clearInterval(devicePollTimer);
|
| 421 |
-
|
| 422 |
-
devicePollTimer = setInterval(async () => {
|
| 423 |
-
try {
|
| 424 |
-
const pollResp = await fetch('/auth/device-poll/' + encodeURIComponent(deviceCode));
|
| 425 |
-
const pollData = await pollResp.json();
|
| 426 |
-
|
| 427 |
-
if (pollData.success) {
|
| 428 |
-
clearInterval(devicePollTimer);
|
| 429 |
-
statusEl.innerHTML = '';
|
| 430 |
-
successEl.textContent = 'Login successful! Redirecting...';
|
| 431 |
-
successEl.style.display = 'block';
|
| 432 |
-
if (pollTimer) clearInterval(pollTimer);
|
| 433 |
-
setTimeout(() => window.location.href = '/', 1000);
|
| 434 |
-
} else if (pollData.error) {
|
| 435 |
-
clearInterval(devicePollTimer);
|
| 436 |
-
statusEl.innerHTML = '';
|
| 437 |
-
errEl.textContent = pollData.error;
|
| 438 |
-
errEl.style.display = 'block';
|
| 439 |
-
}
|
| 440 |
-
// if pollData.pending, keep polling
|
| 441 |
-
} catch {}
|
| 442 |
-
}, interval);
|
| 443 |
-
|
| 444 |
-
// Stop after expiry
|
| 445 |
-
setTimeout(() => {
|
| 446 |
-
if (devicePollTimer) {
|
| 447 |
-
clearInterval(devicePollTimer);
|
| 448 |
-
statusEl.textContent = 'Code expired. Please try again.';
|
| 449 |
-
}
|
| 450 |
-
}, (data.expiresIn || 900) * 1000);
|
| 451 |
-
|
| 452 |
-
} catch (err) {
|
| 453 |
-
btn.innerHTML = 'Sign in with Device Code';
|
| 454 |
-
btn.disabled = false;
|
| 455 |
-
errEl.textContent = err.message;
|
| 456 |
-
errEl.style.display = 'block';
|
| 457 |
-
}
|
| 458 |
-
}
|
| 459 |
-
|
| 460 |
-
// ── CLI Token Import ─────────────────────────────
|
| 461 |
-
async function importCli() {
|
| 462 |
-
const btn = document.getElementById('cliImportBtn');
|
| 463 |
-
const errEl = document.getElementById('cliError');
|
| 464 |
-
const successEl = document.getElementById('cliSuccess');
|
| 465 |
-
errEl.style.display = 'none';
|
| 466 |
-
successEl.style.display = 'none';
|
| 467 |
-
btn.disabled = true;
|
| 468 |
-
btn.textContent = 'Importing...';
|
| 469 |
-
|
| 470 |
-
try {
|
| 471 |
-
const resp = await fetch('/auth/import-cli', { method: 'POST' });
|
| 472 |
-
const data = await resp.json();
|
| 473 |
-
|
| 474 |
-
if (resp.ok && data.success) {
|
| 475 |
-
successEl.textContent = 'CLI token imported! Redirecting...';
|
| 476 |
-
successEl.style.display = 'block';
|
| 477 |
-
if (pollTimer) clearInterval(pollTimer);
|
| 478 |
-
setTimeout(() => window.location.href = '/', 1000);
|
| 479 |
-
} else {
|
| 480 |
-
errEl.textContent = data.error || 'Failed to import CLI token';
|
| 481 |
-
errEl.style.display = 'block';
|
| 482 |
-
}
|
| 483 |
-
} catch (err) {
|
| 484 |
-
errEl.textContent = 'Network error: ' + err.message;
|
| 485 |
-
errEl.style.display = 'block';
|
| 486 |
-
} finally {
|
| 487 |
-
btn.textContent = 'Import CLI Token (~/.codex/auth.json)';
|
| 488 |
-
btn.disabled = false;
|
| 489 |
-
}
|
| 490 |
-
}
|
| 491 |
-
|
| 492 |
-
async function submitToken() {
|
| 493 |
-
const token = document.getElementById('tokenInput').value.trim();
|
| 494 |
-
const errorEl = document.getElementById('errorMsg');
|
| 495 |
-
const successEl = document.getElementById('successMsg');
|
| 496 |
-
errorEl.style.display = 'none';
|
| 497 |
-
successEl.style.display = 'none';
|
| 498 |
-
|
| 499 |
-
if (!token) {
|
| 500 |
-
errorEl.textContent = 'Please paste a token';
|
| 501 |
-
errorEl.style.display = 'block';
|
| 502 |
-
return;
|
| 503 |
-
}
|
| 504 |
-
|
| 505 |
-
try {
|
| 506 |
-
const resp = await fetch('/auth/token', {
|
| 507 |
-
method: 'POST',
|
| 508 |
-
headers: { 'Content-Type': 'application/json' },
|
| 509 |
-
body: JSON.stringify({ token }),
|
| 510 |
-
});
|
| 511 |
-
const data = await resp.json();
|
| 512 |
-
if (resp.ok) {
|
| 513 |
-
successEl.textContent = 'Login successful! Redirecting...';
|
| 514 |
-
successEl.style.display = 'block';
|
| 515 |
-
setTimeout(() => window.location.href = '/', 1000);
|
| 516 |
-
} else {
|
| 517 |
-
errorEl.textContent = data.error || 'Invalid token';
|
| 518 |
-
errorEl.style.display = 'block';
|
| 519 |
-
}
|
| 520 |
-
} catch (err) {
|
| 521 |
-
errorEl.textContent = 'Network error: ' + err.message;
|
| 522 |
-
errorEl.style.display = 'block';
|
| 523 |
-
}
|
| 524 |
-
}
|
| 525 |
-
</script>
|
| 526 |
-
</body>
|
| 527 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/apply-update.ts
DELETED
|
@@ -1,354 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env tsx
|
| 2 |
-
/**
|
| 3 |
-
* apply-update.ts — Compares extracted fingerprint with current config and applies updates.
|
| 4 |
-
*
|
| 5 |
-
* Usage:
|
| 6 |
-
* npx tsx scripts/apply-update.ts [--dry-run]
|
| 7 |
-
*
|
| 8 |
-
* --dry-run: Show what would change without modifying files.
|
| 9 |
-
*/
|
| 10 |
-
|
| 11 |
-
import { readFileSync, writeFileSync, existsSync } from "fs";
|
| 12 |
-
import { resolve } from "path";
|
| 13 |
-
import { createHash } from "crypto";
|
| 14 |
-
import yaml from "js-yaml";
|
| 15 |
-
|
| 16 |
-
const ROOT = resolve(import.meta.dirname, "..");
|
| 17 |
-
const CONFIG_PATH = resolve(ROOT, "config/default.yaml");
|
| 18 |
-
const FINGERPRINT_PATH = resolve(ROOT, "config/fingerprint.yaml");
|
| 19 |
-
const EXTRACTED_PATH = resolve(ROOT, "data/extracted-fingerprint.json");
|
| 20 |
-
const MODELS_PATH = resolve(ROOT, "config/models.yaml");
|
| 21 |
-
const PROMPTS_DIR = resolve(ROOT, "config/prompts");
|
| 22 |
-
|
| 23 |
-
type ChangeType = "auto" | "semi-auto" | "alert";
|
| 24 |
-
|
| 25 |
-
interface Change {
|
| 26 |
-
type: ChangeType;
|
| 27 |
-
category: string;
|
| 28 |
-
description: string;
|
| 29 |
-
current: string;
|
| 30 |
-
updated: string;
|
| 31 |
-
file: string;
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
interface ExtractedFingerprint {
|
| 35 |
-
app_version: string;
|
| 36 |
-
build_number: string;
|
| 37 |
-
api_base_url: string | null;
|
| 38 |
-
originator: string | null;
|
| 39 |
-
models: string[];
|
| 40 |
-
wham_endpoints: string[];
|
| 41 |
-
user_agent_contains: string;
|
| 42 |
-
prompts: {
|
| 43 |
-
desktop_context_hash: string | null;
|
| 44 |
-
desktop_context_path: string | null;
|
| 45 |
-
title_generation_hash: string | null;
|
| 46 |
-
title_generation_path: string | null;
|
| 47 |
-
pr_generation_hash: string | null;
|
| 48 |
-
pr_generation_path: string | null;
|
| 49 |
-
automation_response_hash: string | null;
|
| 50 |
-
automation_response_path: string | null;
|
| 51 |
-
};
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
function loadExtracted(): ExtractedFingerprint {
|
| 55 |
-
if (!existsSync(EXTRACTED_PATH)) {
|
| 56 |
-
throw new Error(
|
| 57 |
-
`No extracted fingerprint found at ${EXTRACTED_PATH}.\n` +
|
| 58 |
-
`Run: npm run extract -- --path <codex-path>`
|
| 59 |
-
);
|
| 60 |
-
}
|
| 61 |
-
return JSON.parse(readFileSync(EXTRACTED_PATH, "utf-8"));
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
function loadCurrentConfig(): Record<string, unknown> {
|
| 65 |
-
return yaml.load(readFileSync(CONFIG_PATH, "utf-8")) as Record<string, unknown>;
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
function detectChanges(extracted: ExtractedFingerprint): Change[] {
|
| 69 |
-
const changes: Change[] = [];
|
| 70 |
-
const config = loadCurrentConfig();
|
| 71 |
-
const client = config.client as Record<string, string>;
|
| 72 |
-
|
| 73 |
-
// Version
|
| 74 |
-
if (extracted.app_version !== client.app_version) {
|
| 75 |
-
changes.push({
|
| 76 |
-
type: "auto",
|
| 77 |
-
category: "version",
|
| 78 |
-
description: "App version changed",
|
| 79 |
-
current: client.app_version,
|
| 80 |
-
updated: extracted.app_version,
|
| 81 |
-
file: CONFIG_PATH,
|
| 82 |
-
});
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
// Build number
|
| 86 |
-
if (extracted.build_number !== client.build_number) {
|
| 87 |
-
changes.push({
|
| 88 |
-
type: "auto",
|
| 89 |
-
category: "build",
|
| 90 |
-
description: "Build number changed",
|
| 91 |
-
current: client.build_number,
|
| 92 |
-
updated: extracted.build_number,
|
| 93 |
-
file: CONFIG_PATH,
|
| 94 |
-
});
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
// Originator
|
| 98 |
-
if (extracted.originator && extracted.originator !== client.originator) {
|
| 99 |
-
changes.push({
|
| 100 |
-
type: "auto",
|
| 101 |
-
category: "originator",
|
| 102 |
-
description: "Originator header changed",
|
| 103 |
-
current: client.originator,
|
| 104 |
-
updated: extracted.originator,
|
| 105 |
-
file: CONFIG_PATH,
|
| 106 |
-
});
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
// API base URL
|
| 110 |
-
const api = config.api as Record<string, string>;
|
| 111 |
-
if (extracted.api_base_url && extracted.api_base_url !== api.base_url) {
|
| 112 |
-
changes.push({
|
| 113 |
-
type: "alert",
|
| 114 |
-
category: "api_url",
|
| 115 |
-
description: "API base URL changed (CRITICAL)",
|
| 116 |
-
current: api.base_url,
|
| 117 |
-
updated: extracted.api_base_url,
|
| 118 |
-
file: CONFIG_PATH,
|
| 119 |
-
});
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
// Models — compare against config/models.yaml
|
| 123 |
-
const modelsYaml = yaml.load(readFileSync(MODELS_PATH, "utf-8")) as {
|
| 124 |
-
models: { id: string }[];
|
| 125 |
-
};
|
| 126 |
-
const currentModels = modelsYaml.models.map((m) => m.id);
|
| 127 |
-
const extractedModels = extracted.models.filter((m) => m.includes("codex") || currentModels.includes(m));
|
| 128 |
-
|
| 129 |
-
const newModels = extractedModels.filter((m) => !currentModels.includes(m));
|
| 130 |
-
const removedModels = currentModels.filter((m) => !extractedModels.includes(m));
|
| 131 |
-
|
| 132 |
-
if (newModels.length > 0) {
|
| 133 |
-
changes.push({
|
| 134 |
-
type: "semi-auto",
|
| 135 |
-
category: "models_added",
|
| 136 |
-
description: `New models found: ${newModels.join(", ")}`,
|
| 137 |
-
current: currentModels.join(", "),
|
| 138 |
-
updated: extractedModels.join(", "),
|
| 139 |
-
file: MODELS_PATH,
|
| 140 |
-
});
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
if (removedModels.length > 0) {
|
| 144 |
-
changes.push({
|
| 145 |
-
type: "semi-auto",
|
| 146 |
-
category: "models_removed",
|
| 147 |
-
description: `Models removed: ${removedModels.join(", ")}`,
|
| 148 |
-
current: currentModels.join(", "),
|
| 149 |
-
updated: extractedModels.join(", "),
|
| 150 |
-
file: MODELS_PATH,
|
| 151 |
-
});
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
// WHAM endpoints — check for new ones
|
| 155 |
-
const knownEndpoints = [
|
| 156 |
-
"/wham/tasks", "/wham/environments", "/wham/accounts/check",
|
| 157 |
-
"/wham/usage",
|
| 158 |
-
];
|
| 159 |
-
const newEndpoints = extracted.wham_endpoints.filter(
|
| 160 |
-
(ep) => !knownEndpoints.some((k) => ep.startsWith(k))
|
| 161 |
-
);
|
| 162 |
-
if (newEndpoints.length > 0) {
|
| 163 |
-
changes.push({
|
| 164 |
-
type: "alert",
|
| 165 |
-
category: "endpoints",
|
| 166 |
-
description: `New WHAM endpoints found: ${newEndpoints.join(", ")}`,
|
| 167 |
-
current: "(known set)",
|
| 168 |
-
updated: newEndpoints.join(", "),
|
| 169 |
-
file: "src/proxy/wham-api.ts",
|
| 170 |
-
});
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
// System prompts — check hash changes
|
| 174 |
-
const promptConfigs = [
|
| 175 |
-
{ name: "desktop-context", hash: extracted.prompts.desktop_context_hash, path: extracted.prompts.desktop_context_path },
|
| 176 |
-
{ name: "title-generation", hash: extracted.prompts.title_generation_hash, path: extracted.prompts.title_generation_path },
|
| 177 |
-
{ name: "pr-generation", hash: extracted.prompts.pr_generation_hash, path: extracted.prompts.pr_generation_path },
|
| 178 |
-
{ name: "automation-response", hash: extracted.prompts.automation_response_hash, path: extracted.prompts.automation_response_path },
|
| 179 |
-
];
|
| 180 |
-
|
| 181 |
-
for (const { name, hash, path } of promptConfigs) {
|
| 182 |
-
if (!hash || !path) continue;
|
| 183 |
-
|
| 184 |
-
const configPromptPath = resolve(PROMPTS_DIR, `${name}.md`);
|
| 185 |
-
if (!existsSync(configPromptPath)) {
|
| 186 |
-
changes.push({
|
| 187 |
-
type: "semi-auto",
|
| 188 |
-
category: `prompt_${name}`,
|
| 189 |
-
description: `New prompt file: ${name}.md (not in config/prompts/)`,
|
| 190 |
-
current: "(missing)",
|
| 191 |
-
updated: hash,
|
| 192 |
-
file: configPromptPath,
|
| 193 |
-
});
|
| 194 |
-
continue;
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
const currentContent = readFileSync(configPromptPath, "utf-8");
|
| 198 |
-
const currentHash = `sha256:${createHash("sha256").update(currentContent).digest("hex").slice(0, 16)}`;
|
| 199 |
-
|
| 200 |
-
if (currentHash !== hash) {
|
| 201 |
-
changes.push({
|
| 202 |
-
type: "semi-auto",
|
| 203 |
-
category: `prompt_${name}`,
|
| 204 |
-
description: `System prompt changed: ${name}`,
|
| 205 |
-
current: currentHash,
|
| 206 |
-
updated: hash,
|
| 207 |
-
file: configPromptPath,
|
| 208 |
-
});
|
| 209 |
-
}
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
return changes;
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
function applyAutoChanges(changes: Change[], dryRun: boolean): void {
|
| 216 |
-
const autoChanges = changes.filter((c) => c.type === "auto");
|
| 217 |
-
|
| 218 |
-
if (autoChanges.length === 0) {
|
| 219 |
-
console.log("\n No auto-applicable changes.");
|
| 220 |
-
return;
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
// Group config changes
|
| 224 |
-
const configChanges = autoChanges.filter((c) => c.file === CONFIG_PATH);
|
| 225 |
-
|
| 226 |
-
if (configChanges.length > 0 && !dryRun) {
|
| 227 |
-
let configContent = readFileSync(CONFIG_PATH, "utf-8");
|
| 228 |
-
|
| 229 |
-
for (const change of configChanges) {
|
| 230 |
-
switch (change.category) {
|
| 231 |
-
case "version":
|
| 232 |
-
configContent = configContent.replace(
|
| 233 |
-
/app_version:\s*"[^"]+"/,
|
| 234 |
-
`app_version: "${change.updated}"`,
|
| 235 |
-
);
|
| 236 |
-
break;
|
| 237 |
-
case "build":
|
| 238 |
-
configContent = configContent.replace(
|
| 239 |
-
/build_number:\s*"[^"]+"/,
|
| 240 |
-
`build_number: "${change.updated}"`,
|
| 241 |
-
);
|
| 242 |
-
break;
|
| 243 |
-
case "originator":
|
| 244 |
-
configContent = configContent.replace(
|
| 245 |
-
/originator:\s*"[^"]+"/,
|
| 246 |
-
`originator: "${change.updated}"`,
|
| 247 |
-
);
|
| 248 |
-
break;
|
| 249 |
-
}
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
writeFileSync(CONFIG_PATH, configContent);
|
| 253 |
-
console.log(` [APPLIED] config/default.yaml updated`);
|
| 254 |
-
}
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
function displayReport(changes: Change[], dryRun: boolean): void {
|
| 258 |
-
console.log("\n╔══════════════════════════════════════════╗");
|
| 259 |
-
console.log(`║ Update Analysis ${dryRun ? "(DRY RUN)" : ""} ║`);
|
| 260 |
-
console.log("╠══════════════════════════════════════════╣");
|
| 261 |
-
|
| 262 |
-
if (changes.length === 0) {
|
| 263 |
-
console.log("║ No changes detected — up to date! ║");
|
| 264 |
-
console.log("╚══════════════════════════════════════════╝");
|
| 265 |
-
return;
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
console.log("╚══════════════════════════════════════════╝\n");
|
| 269 |
-
|
| 270 |
-
// Auto changes
|
| 271 |
-
const auto = changes.filter((c) => c.type === "auto");
|
| 272 |
-
if (auto.length > 0) {
|
| 273 |
-
console.log(` AUTO-APPLY (${auto.length}):`);
|
| 274 |
-
for (const c of auto) {
|
| 275 |
-
const action = dryRun ? "WOULD APPLY" : "APPLIED";
|
| 276 |
-
console.log(` [${action}] ${c.description}`);
|
| 277 |
-
console.log(` ${c.current} → ${c.updated}`);
|
| 278 |
-
}
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
// Semi-auto changes
|
| 282 |
-
const semi = changes.filter((c) => c.type === "semi-auto");
|
| 283 |
-
if (semi.length > 0) {
|
| 284 |
-
console.log(`\n SEMI-AUTO (needs review) (${semi.length}):`);
|
| 285 |
-
for (const c of semi) {
|
| 286 |
-
console.log(` [REVIEW] ${c.description}`);
|
| 287 |
-
console.log(` File: ${c.file}`);
|
| 288 |
-
console.log(` Current: ${c.current}`);
|
| 289 |
-
console.log(` New: ${c.updated}`);
|
| 290 |
-
}
|
| 291 |
-
}
|
| 292 |
-
|
| 293 |
-
// Alerts
|
| 294 |
-
const alerts = changes.filter((c) => c.type === "alert");
|
| 295 |
-
if (alerts.length > 0) {
|
| 296 |
-
console.log(`\n *** ALERTS (${alerts.length}) ***`);
|
| 297 |
-
for (const c of alerts) {
|
| 298 |
-
console.log(` [ALERT] ${c.description}`);
|
| 299 |
-
console.log(` File: ${c.file}`);
|
| 300 |
-
console.log(` Current: ${c.current}`);
|
| 301 |
-
console.log(` New: ${c.updated}`);
|
| 302 |
-
}
|
| 303 |
-
}
|
| 304 |
-
|
| 305 |
-
// Prompt diffs
|
| 306 |
-
const promptChanges = changes.filter((c) => c.category.startsWith("prompt_"));
|
| 307 |
-
if (promptChanges.length > 0) {
|
| 308 |
-
console.log("\n PROMPT CHANGES:");
|
| 309 |
-
console.log(" To apply prompt updates, copy from data/extracted-prompts/ to config/prompts/:");
|
| 310 |
-
for (const c of promptChanges) {
|
| 311 |
-
const name = c.category.replace("prompt_", "");
|
| 312 |
-
console.log(` cp data/extracted-prompts/${name}.md config/prompts/${name}.md`);
|
| 313 |
-
}
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
console.log("");
|
| 317 |
-
}
|
| 318 |
-
|
| 319 |
-
async function main() {
|
| 320 |
-
const dryRun = process.argv.includes("--dry-run");
|
| 321 |
-
|
| 322 |
-
console.log("[apply-update] Loading extracted fingerprint...");
|
| 323 |
-
const extracted = loadExtracted();
|
| 324 |
-
console.log(` Extracted: v${extracted.app_version} (build ${extracted.build_number})`);
|
| 325 |
-
|
| 326 |
-
console.log("[apply-update] Comparing with current config...");
|
| 327 |
-
const changes = detectChanges(extracted);
|
| 328 |
-
|
| 329 |
-
displayReport(changes, dryRun);
|
| 330 |
-
|
| 331 |
-
if (!dryRun) {
|
| 332 |
-
applyAutoChanges(changes, dryRun);
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
// Summary
|
| 336 |
-
const auto = changes.filter((c) => c.type === "auto").length;
|
| 337 |
-
const semi = changes.filter((c) => c.type === "semi-auto").length;
|
| 338 |
-
const alerts = changes.filter((c) => c.type === "alert").length;
|
| 339 |
-
|
| 340 |
-
console.log(`[apply-update] Summary: ${auto} auto, ${semi} semi-auto, ${alerts} alerts`);
|
| 341 |
-
|
| 342 |
-
if (semi > 0 || alerts > 0) {
|
| 343 |
-
console.log("[apply-update] Manual review needed for semi-auto and alert items above.");
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
if (dryRun && auto > 0) {
|
| 347 |
-
console.log("[apply-update] Run without --dry-run to apply auto changes.");
|
| 348 |
-
}
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
main().catch((err) => {
|
| 352 |
-
console.error("[apply-update] Fatal:", err);
|
| 353 |
-
process.exit(1);
|
| 354 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/check-update.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env tsx
|
| 2 |
-
/**
|
| 3 |
-
* check-update.ts — Polls the Codex Sparkle appcast feed for new versions.
|
| 4 |
-
*
|
| 5 |
-
* Usage:
|
| 6 |
-
* npx tsx scripts/check-update.ts [--watch]
|
| 7 |
-
*
|
| 8 |
-
* With --watch: polls every 30 minutes and keeps running.
|
| 9 |
-
* Without: runs once and exits.
|
| 10 |
-
*/
|
| 11 |
-
|
| 12 |
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
| 13 |
-
import { resolve } from "path";
|
| 14 |
-
import yaml from "js-yaml";
|
| 15 |
-
|
| 16 |
-
const ROOT = resolve(import.meta.dirname, "..");
|
| 17 |
-
const CONFIG_PATH = resolve(ROOT, "config/default.yaml");
|
| 18 |
-
const STATE_PATH = resolve(ROOT, "data/update-state.json");
|
| 19 |
-
const APPCAST_URL = "https://persistent.oaistatic.com/codex-app-prod/appcast.xml";
|
| 20 |
-
const POLL_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
|
| 21 |
-
|
| 22 |
-
interface UpdateState {
|
| 23 |
-
last_check: string;
|
| 24 |
-
latest_version: string | null;
|
| 25 |
-
latest_build: string | null;
|
| 26 |
-
download_url: string | null;
|
| 27 |
-
update_available: boolean;
|
| 28 |
-
current_version: string;
|
| 29 |
-
current_build: string;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
interface CurrentConfig {
|
| 33 |
-
app_version: string;
|
| 34 |
-
build_number: string;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
function loadCurrentConfig(): CurrentConfig {
|
| 38 |
-
const raw = yaml.load(readFileSync(CONFIG_PATH, "utf-8")) as Record<string, unknown>;
|
| 39 |
-
const client = raw.client as Record<string, string>;
|
| 40 |
-
return {
|
| 41 |
-
app_version: client.app_version,
|
| 42 |
-
build_number: client.build_number,
|
| 43 |
-
};
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
/**
|
| 47 |
-
* Parse appcast XML to extract version info.
|
| 48 |
-
* Uses regex-based parsing to avoid heavy XML dependencies.
|
| 49 |
-
*/
|
| 50 |
-
function parseAppcast(xml: string): {
|
| 51 |
-
version: string | null;
|
| 52 |
-
build: string | null;
|
| 53 |
-
downloadUrl: string | null;
|
| 54 |
-
} {
|
| 55 |
-
// Extract the latest <item> (first one is usually the latest)
|
| 56 |
-
const itemMatch = xml.match(/<item>([\s\S]*?)<\/item>/i);
|
| 57 |
-
if (!itemMatch) {
|
| 58 |
-
return { version: null, build: null, downloadUrl: null };
|
| 59 |
-
}
|
| 60 |
-
const item = itemMatch[1];
|
| 61 |
-
|
| 62 |
-
// sparkle:shortVersionString = display version
|
| 63 |
-
const versionMatch = item.match(/sparkle:shortVersionString="([^"]+)"/);
|
| 64 |
-
// sparkle:version = build number
|
| 65 |
-
const buildMatch = item.match(/sparkle:version="([^"]+)"/);
|
| 66 |
-
// url = download URL from enclosure
|
| 67 |
-
const urlMatch = item.match(/url="([^"]+)"/);
|
| 68 |
-
|
| 69 |
-
return {
|
| 70 |
-
version: versionMatch?.[1] ?? null,
|
| 71 |
-
build: buildMatch?.[1] ?? null,
|
| 72 |
-
downloadUrl: urlMatch?.[1] ?? null,
|
| 73 |
-
};
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
async function checkOnce(): Promise<UpdateState> {
|
| 77 |
-
const current = loadCurrentConfig();
|
| 78 |
-
|
| 79 |
-
console.log(`[check-update] Current: v${current.app_version} (build ${current.build_number})`);
|
| 80 |
-
console.log(`[check-update] Fetching ${APPCAST_URL}...`);
|
| 81 |
-
|
| 82 |
-
const res = await fetch(APPCAST_URL);
|
| 83 |
-
if (!res.ok) {
|
| 84 |
-
throw new Error(`Failed to fetch appcast: ${res.status} ${res.statusText}`);
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
const xml = await res.text();
|
| 88 |
-
const { version, build, downloadUrl } = parseAppcast(xml);
|
| 89 |
-
|
| 90 |
-
if (!version || !build) {
|
| 91 |
-
console.warn("[check-update] Could not parse version from appcast");
|
| 92 |
-
return {
|
| 93 |
-
last_check: new Date().toISOString(),
|
| 94 |
-
latest_version: null,
|
| 95 |
-
latest_build: null,
|
| 96 |
-
download_url: null,
|
| 97 |
-
update_available: false,
|
| 98 |
-
current_version: current.app_version,
|
| 99 |
-
current_build: current.build_number,
|
| 100 |
-
};
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
const updateAvailable =
|
| 104 |
-
version !== current.app_version || build !== current.build_number;
|
| 105 |
-
|
| 106 |
-
const state: UpdateState = {
|
| 107 |
-
last_check: new Date().toISOString(),
|
| 108 |
-
latest_version: version,
|
| 109 |
-
latest_build: build,
|
| 110 |
-
download_url: downloadUrl,
|
| 111 |
-
update_available: updateAvailable,
|
| 112 |
-
current_version: current.app_version,
|
| 113 |
-
current_build: current.build_number,
|
| 114 |
-
};
|
| 115 |
-
|
| 116 |
-
if (updateAvailable) {
|
| 117 |
-
console.log(`\n *** UPDATE AVAILABLE ***`);
|
| 118 |
-
console.log(` New version: ${version} (build ${build})`);
|
| 119 |
-
console.log(` Current: ${current.app_version} (build ${current.build_number})`);
|
| 120 |
-
if (downloadUrl) {
|
| 121 |
-
console.log(` Download: ${downloadUrl}`);
|
| 122 |
-
}
|
| 123 |
-
console.log(`\n Run: npm run extract -- --path <new-codex-path>`);
|
| 124 |
-
console.log(` Then: npm run apply-update\n`);
|
| 125 |
-
} else {
|
| 126 |
-
console.log(`[check-update] Up to date: v${version} (build ${build})`);
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
// Write state
|
| 130 |
-
mkdirSync(resolve(ROOT, "data"), { recursive: true });
|
| 131 |
-
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
|
| 132 |
-
console.log(`[check-update] State written to ${STATE_PATH}`);
|
| 133 |
-
|
| 134 |
-
return state;
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
async function main() {
|
| 138 |
-
const watch = process.argv.includes("--watch");
|
| 139 |
-
|
| 140 |
-
await checkOnce();
|
| 141 |
-
|
| 142 |
-
if (watch) {
|
| 143 |
-
console.log(`[check-update] Watching for updates every ${POLL_INTERVAL_MS / 60000} minutes...`);
|
| 144 |
-
setInterval(async () => {
|
| 145 |
-
try {
|
| 146 |
-
await checkOnce();
|
| 147 |
-
} catch (err) {
|
| 148 |
-
console.error("[check-update] Poll error:", err);
|
| 149 |
-
}
|
| 150 |
-
}, POLL_INTERVAL_MS);
|
| 151 |
-
}
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
main().catch((err) => {
|
| 155 |
-
console.error("[check-update] Fatal:", err);
|
| 156 |
-
process.exit(1);
|
| 157 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/extract-fingerprint.ts
DELETED
|
@@ -1,445 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env tsx
|
| 2 |
-
/**
|
| 3 |
-
* extract-fingerprint.ts — Extracts key fingerprint values from a Codex Desktop
|
| 4 |
-
* installation (macOS .app or Windows extracted ASAR).
|
| 5 |
-
*
|
| 6 |
-
* Usage:
|
| 7 |
-
* npx tsx scripts/extract-fingerprint.ts --path "C:/path/to/Codex" [--asar-out ./asar-out]
|
| 8 |
-
*
|
| 9 |
-
* The path can point to:
|
| 10 |
-
* - A macOS .app bundle (Codex.app)
|
| 11 |
-
* - A directory containing an already-extracted ASAR (with package.json and .vite/build/main.js)
|
| 12 |
-
* - A Windows install dir containing resources/app.asar
|
| 13 |
-
*/
|
| 14 |
-
|
| 15 |
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
| 16 |
-
import { resolve, join } from "path";
|
| 17 |
-
import { createHash } from "crypto";
|
| 18 |
-
import { execSync } from "child_process";
|
| 19 |
-
import yaml from "js-yaml";
|
| 20 |
-
|
| 21 |
-
const ROOT = resolve(import.meta.dirname, "..");
|
| 22 |
-
const OUTPUT_PATH = resolve(ROOT, "data/extracted-fingerprint.json");
|
| 23 |
-
const PROMPTS_DIR = resolve(ROOT, "data/extracted-prompts");
|
| 24 |
-
const PATTERNS_PATH = resolve(ROOT, "config/extraction-patterns.yaml");
|
| 25 |
-
|
| 26 |
-
interface ExtractionPatterns {
|
| 27 |
-
package_json: { version_key: string; build_number_key: string; sparkle_feed_key: string };
|
| 28 |
-
main_js: Record<string, {
|
| 29 |
-
pattern?: string;
|
| 30 |
-
group?: number;
|
| 31 |
-
global?: boolean;
|
| 32 |
-
start_marker?: string;
|
| 33 |
-
end_marker?: string;
|
| 34 |
-
end_pattern?: string;
|
| 35 |
-
description: string;
|
| 36 |
-
}>;
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
interface ExtractedFingerprint {
|
| 40 |
-
app_version: string;
|
| 41 |
-
build_number: string;
|
| 42 |
-
api_base_url: string | null;
|
| 43 |
-
originator: string | null;
|
| 44 |
-
models: string[];
|
| 45 |
-
wham_endpoints: string[];
|
| 46 |
-
user_agent_contains: string;
|
| 47 |
-
sparkle_feed_url: string | null;
|
| 48 |
-
prompts: {
|
| 49 |
-
desktop_context_hash: string | null;
|
| 50 |
-
desktop_context_path: string | null;
|
| 51 |
-
title_generation_hash: string | null;
|
| 52 |
-
title_generation_path: string | null;
|
| 53 |
-
pr_generation_hash: string | null;
|
| 54 |
-
pr_generation_path: string | null;
|
| 55 |
-
automation_response_hash: string | null;
|
| 56 |
-
automation_response_path: string | null;
|
| 57 |
-
};
|
| 58 |
-
extracted_at: string;
|
| 59 |
-
source_path: string;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
function sha256(content: string): string {
|
| 63 |
-
return `sha256:${createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16)}`;
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
function loadPatterns(): ExtractionPatterns {
|
| 67 |
-
const raw = yaml.load(readFileSync(PATTERNS_PATH, "utf-8")) as ExtractionPatterns;
|
| 68 |
-
return raw;
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
/**
|
| 72 |
-
* Find the extracted ASAR root given an input path.
|
| 73 |
-
* Tries multiple layout conventions.
|
| 74 |
-
*/
|
| 75 |
-
function findAsarRoot(inputPath: string): string {
|
| 76 |
-
// Direct: path has package.json (already extracted)
|
| 77 |
-
if (existsSync(join(inputPath, "package.json"))) {
|
| 78 |
-
return inputPath;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
// macOS .app bundle
|
| 82 |
-
const macResources = join(inputPath, "Contents/Resources");
|
| 83 |
-
if (existsSync(join(macResources, "app.asar"))) {
|
| 84 |
-
return extractAsar(join(macResources, "app.asar"));
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
// Windows: resources/app.asar
|
| 88 |
-
const winResources = join(inputPath, "resources");
|
| 89 |
-
if (existsSync(join(winResources, "app.asar"))) {
|
| 90 |
-
return extractAsar(join(winResources, "app.asar"));
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
// Already extracted: check for nested 'extracted' dir
|
| 94 |
-
const extractedDir = join(inputPath, "extracted");
|
| 95 |
-
if (existsSync(join(extractedDir, "package.json"))) {
|
| 96 |
-
return extractedDir;
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
// Check recovered/extracted pattern
|
| 100 |
-
const recoveredExtracted = join(inputPath, "recovered/extracted");
|
| 101 |
-
if (existsSync(join(recoveredExtracted, "package.json"))) {
|
| 102 |
-
return recoveredExtracted;
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
throw new Error(
|
| 106 |
-
`Cannot find Codex source at ${inputPath}. Expected package.json or app.asar.`
|
| 107 |
-
);
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
function extractAsar(asarPath: string): string {
|
| 111 |
-
const outDir = resolve(ROOT, ".asar-out");
|
| 112 |
-
console.log(`[extract] Extracting ASAR: ${asarPath} → ${outDir}`);
|
| 113 |
-
execSync(`npx @electron/asar extract "${asarPath}" "${outDir}"`, {
|
| 114 |
-
stdio: "inherit",
|
| 115 |
-
});
|
| 116 |
-
return outDir;
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
/**
|
| 120 |
-
* Step A: Extract from package.json
|
| 121 |
-
*/
|
| 122 |
-
function extractFromPackageJson(root: string): {
|
| 123 |
-
version: string;
|
| 124 |
-
buildNumber: string;
|
| 125 |
-
sparkleFeedUrl: string | null;
|
| 126 |
-
} {
|
| 127 |
-
const pkgPath = join(root, "package.json");
|
| 128 |
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
| 129 |
-
|
| 130 |
-
return {
|
| 131 |
-
version: pkg.version ?? "unknown",
|
| 132 |
-
buildNumber: String(pkg.codexBuildNumber ?? "unknown"),
|
| 133 |
-
sparkleFeedUrl: pkg.codexSparkleFeedUrl ?? null,
|
| 134 |
-
};
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
/**
|
| 138 |
-
* Step B: Extract values from main.js using patterns
|
| 139 |
-
*/
|
| 140 |
-
function extractFromMainJs(
|
| 141 |
-
content: string,
|
| 142 |
-
patterns: ExtractionPatterns["main_js"],
|
| 143 |
-
): {
|
| 144 |
-
apiBaseUrl: string | null;
|
| 145 |
-
originator: string | null;
|
| 146 |
-
models: string[];
|
| 147 |
-
whamEndpoints: string[];
|
| 148 |
-
userAgentContains: string;
|
| 149 |
-
} {
|
| 150 |
-
// API base URL
|
| 151 |
-
let apiBaseUrl: string | null = null;
|
| 152 |
-
const apiPattern = patterns.api_base_url;
|
| 153 |
-
if (apiPattern?.pattern) {
|
| 154 |
-
const m = content.match(new RegExp(apiPattern.pattern));
|
| 155 |
-
if (m) apiBaseUrl = m[0];
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
// Fail fast on critical fields
|
| 159 |
-
if (!apiBaseUrl) {
|
| 160 |
-
console.error("[extract] CRITICAL: Failed to extract API base URL from main.js");
|
| 161 |
-
console.error("[extract] The extraction pattern may need updating for this version.");
|
| 162 |
-
throw new Error("Failed to extract critical field: api_base_url");
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
// Originator
|
| 166 |
-
let originator: string | null = null;
|
| 167 |
-
const origPattern = patterns.originator;
|
| 168 |
-
if (origPattern?.pattern) {
|
| 169 |
-
const m = content.match(new RegExp(origPattern.pattern));
|
| 170 |
-
if (m) originator = m[origPattern.group ?? 0] ?? m[0];
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
// Fail fast on critical fields
|
| 174 |
-
if (!originator) {
|
| 175 |
-
console.error("[extract] CRITICAL: Failed to extract originator from main.js");
|
| 176 |
-
console.error("[extract] The extraction pattern may need updating for this version.");
|
| 177 |
-
throw new Error("Failed to extract critical field: originator");
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
// Models — deduplicate, use capture group if specified
|
| 181 |
-
const models: Set<string> = new Set();
|
| 182 |
-
const modelPattern = patterns.models;
|
| 183 |
-
if (modelPattern?.pattern) {
|
| 184 |
-
const re = new RegExp(modelPattern.pattern, "g");
|
| 185 |
-
const groupIdx = modelPattern.group ?? 0;
|
| 186 |
-
for (const m of content.matchAll(re)) {
|
| 187 |
-
models.add(m[groupIdx] ?? m[0]);
|
| 188 |
-
}
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
// WHAM endpoints — deduplicate, use capture group if specified
|
| 192 |
-
const endpoints: Set<string> = new Set();
|
| 193 |
-
const epPattern = patterns.wham_endpoints;
|
| 194 |
-
if (epPattern?.pattern) {
|
| 195 |
-
const re = new RegExp(epPattern.pattern, "g");
|
| 196 |
-
const epGroupIdx = epPattern.group ?? 0;
|
| 197 |
-
for (const m of content.matchAll(re)) {
|
| 198 |
-
endpoints.add(m[epGroupIdx] ?? m[0]);
|
| 199 |
-
}
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
return {
|
| 203 |
-
apiBaseUrl,
|
| 204 |
-
originator,
|
| 205 |
-
models: [...models].sort(),
|
| 206 |
-
whamEndpoints: [...endpoints].sort(),
|
| 207 |
-
userAgentContains: "Codex Desktop/",
|
| 208 |
-
};
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
/**
|
| 212 |
-
* Step B (continued): Extract system prompts from main.js
|
| 213 |
-
*/
|
| 214 |
-
function extractPrompts(content: string): {
|
| 215 |
-
desktopContext: string | null;
|
| 216 |
-
titleGeneration: string | null;
|
| 217 |
-
prGeneration: string | null;
|
| 218 |
-
automationResponse: string | null;
|
| 219 |
-
} {
|
| 220 |
-
// Desktop context: from "# Codex desktop context" to the end of the template literal
|
| 221 |
-
let desktopContext: string | null = null;
|
| 222 |
-
const dcStart = content.indexOf("# Codex desktop context");
|
| 223 |
-
if (dcStart !== -1) {
|
| 224 |
-
// Find the closing backtick of the template literal
|
| 225 |
-
// Look backwards from dcStart for the opening backtick to understand nesting
|
| 226 |
-
// Then scan forward for the matching close
|
| 227 |
-
const afterStart = content.indexOf("`;", dcStart);
|
| 228 |
-
if (afterStart !== -1) {
|
| 229 |
-
desktopContext = content.slice(dcStart, afterStart).trim();
|
| 230 |
-
}
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
// Title generation: from the function that builds the array
|
| 234 |
-
let titleGeneration: string | null = null;
|
| 235 |
-
const titleMarker = "You are a helpful assistant. You will be presented with a user prompt";
|
| 236 |
-
const titleStart = content.indexOf(titleMarker);
|
| 237 |
-
if (titleStart !== -1) {
|
| 238 |
-
// Find the enclosing array end: ].join(
|
| 239 |
-
const joinIdx = content.indexOf("].join(", titleStart);
|
| 240 |
-
if (joinIdx !== -1) {
|
| 241 |
-
// Extract the array content between [ and ]
|
| 242 |
-
const bracketStart = content.lastIndexOf("[", titleStart);
|
| 243 |
-
if (bracketStart !== -1) {
|
| 244 |
-
const arrayContent = content.slice(bracketStart + 1, joinIdx);
|
| 245 |
-
// Parse string literals from the array
|
| 246 |
-
titleGeneration = parseStringArray(arrayContent);
|
| 247 |
-
}
|
| 248 |
-
}
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
// PR generation
|
| 252 |
-
let prGeneration: string | null = null;
|
| 253 |
-
const prMarker = "You are a helpful assistant. Generate a pull request title";
|
| 254 |
-
const prStart = content.indexOf(prMarker);
|
| 255 |
-
if (prStart !== -1) {
|
| 256 |
-
const joinIdx = content.indexOf("].join(", prStart);
|
| 257 |
-
if (joinIdx !== -1) {
|
| 258 |
-
const bracketStart = content.lastIndexOf("[", prStart);
|
| 259 |
-
if (bracketStart !== -1) {
|
| 260 |
-
const arrayContent = content.slice(bracketStart + 1, joinIdx);
|
| 261 |
-
prGeneration = parseStringArray(arrayContent);
|
| 262 |
-
}
|
| 263 |
-
}
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
// Automation response: template literal starting with "Response MUST end with"
|
| 267 |
-
let automationResponse: string | null = null;
|
| 268 |
-
const autoMarker = "Response MUST end with a remark-directive block";
|
| 269 |
-
const autoStart = content.indexOf(autoMarker);
|
| 270 |
-
if (autoStart !== -1) {
|
| 271 |
-
const afterAuto = content.indexOf("`;", autoStart);
|
| 272 |
-
if (afterAuto !== -1) {
|
| 273 |
-
automationResponse = content.slice(autoStart, afterAuto).trim();
|
| 274 |
-
}
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
return { desktopContext, titleGeneration, prGeneration, automationResponse };
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
/**
|
| 281 |
-
* Parse a JavaScript string array content into a single joined string.
|
| 282 |
-
* Handles simple quoted strings separated by commas.
|
| 283 |
-
*/
|
| 284 |
-
function parseStringArray(arrayContent: string): string {
|
| 285 |
-
const lines: string[] = [];
|
| 286 |
-
// Match quoted strings (both single and double quotes) and template literals
|
| 287 |
-
const stringRe = /"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'/g;
|
| 288 |
-
for (const m of arrayContent.matchAll(stringRe)) {
|
| 289 |
-
const str = m[1] ?? m[2] ?? "";
|
| 290 |
-
// Unescape common sequences
|
| 291 |
-
lines.push(
|
| 292 |
-
str
|
| 293 |
-
.replace(/\\n/g, "\n")
|
| 294 |
-
.replace(/\\t/g, "\t")
|
| 295 |
-
.replace(/\\"/g, '"')
|
| 296 |
-
.replace(/\\'/g, "'")
|
| 297 |
-
.replace(/\\\\/g, "\\")
|
| 298 |
-
);
|
| 299 |
-
}
|
| 300 |
-
return lines.join("\n");
|
| 301 |
-
}
|
| 302 |
-
|
| 303 |
-
function savePrompt(name: string, content: string | null): { hash: string | null; path: string | null } {
|
| 304 |
-
if (!content) return { hash: null, path: null };
|
| 305 |
-
|
| 306 |
-
mkdirSync(PROMPTS_DIR, { recursive: true });
|
| 307 |
-
const filePath = join(PROMPTS_DIR, `${name}.md`);
|
| 308 |
-
writeFileSync(filePath, content);
|
| 309 |
-
|
| 310 |
-
return {
|
| 311 |
-
hash: sha256(content),
|
| 312 |
-
path: filePath,
|
| 313 |
-
};
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
async function main() {
|
| 317 |
-
// Parse --path argument
|
| 318 |
-
const pathIdx = process.argv.indexOf("--path");
|
| 319 |
-
if (pathIdx === -1 || !process.argv[pathIdx + 1]) {
|
| 320 |
-
console.error("Usage: npx tsx scripts/extract-fingerprint.ts --path <codex-path>");
|
| 321 |
-
console.error("");
|
| 322 |
-
console.error(" <codex-path> can be:");
|
| 323 |
-
console.error(" - macOS: /path/to/Codex.app");
|
| 324 |
-
console.error(" - Windows: C:/path/to/Codex (containing resources/app.asar)");
|
| 325 |
-
console.error(" - Extracted: directory with package.json and .vite/build/main.js");
|
| 326 |
-
process.exit(1);
|
| 327 |
-
}
|
| 328 |
-
|
| 329 |
-
const inputPath = resolve(process.argv[pathIdx + 1]);
|
| 330 |
-
console.log(`[extract] Input: ${inputPath}`);
|
| 331 |
-
|
| 332 |
-
// Find ASAR root
|
| 333 |
-
const asarRoot = findAsarRoot(inputPath);
|
| 334 |
-
console.log(`[extract] ASAR root: ${asarRoot}`);
|
| 335 |
-
|
| 336 |
-
// Load extraction patterns
|
| 337 |
-
const patterns = loadPatterns();
|
| 338 |
-
|
| 339 |
-
// Step A: package.json
|
| 340 |
-
console.log("[extract] Reading package.json...");
|
| 341 |
-
const { version, buildNumber, sparkleFeedUrl } = extractFromPackageJson(asarRoot);
|
| 342 |
-
console.log(` version: ${version}`);
|
| 343 |
-
console.log(` build: ${buildNumber}`);
|
| 344 |
-
|
| 345 |
-
// Step B: main.js
|
| 346 |
-
console.log("[extract] Loading main.js...");
|
| 347 |
-
const mainJs = await (async () => {
|
| 348 |
-
const mainPath = join(asarRoot, ".vite/build/main.js");
|
| 349 |
-
if (!existsSync(mainPath)) {
|
| 350 |
-
console.warn("[extract] main.js not found, skipping JS extraction");
|
| 351 |
-
return null;
|
| 352 |
-
}
|
| 353 |
-
|
| 354 |
-
const content = readFileSync(mainPath, "utf-8");
|
| 355 |
-
const lineCount = content.split("\n").length;
|
| 356 |
-
|
| 357 |
-
if (lineCount < 100 && content.length > 100000) {
|
| 358 |
-
console.log("[extract] main.js appears minified, attempting beautify...");
|
| 359 |
-
try {
|
| 360 |
-
const jsBeautify = await import("js-beautify");
|
| 361 |
-
return jsBeautify.default.js(content, { indent_size: 2 });
|
| 362 |
-
} catch {
|
| 363 |
-
console.warn("[extract] js-beautify not available, using raw content");
|
| 364 |
-
return content;
|
| 365 |
-
}
|
| 366 |
-
}
|
| 367 |
-
return content;
|
| 368 |
-
})();
|
| 369 |
-
|
| 370 |
-
let mainJsResults = {
|
| 371 |
-
apiBaseUrl: null as string | null,
|
| 372 |
-
originator: null as string | null,
|
| 373 |
-
models: [] as string[],
|
| 374 |
-
whamEndpoints: [] as string[],
|
| 375 |
-
userAgentContains: "Codex Desktop/",
|
| 376 |
-
};
|
| 377 |
-
|
| 378 |
-
let promptResults = {
|
| 379 |
-
desktopContext: null as string | null,
|
| 380 |
-
titleGeneration: null as string | null,
|
| 381 |
-
prGeneration: null as string | null,
|
| 382 |
-
automationResponse: null as string | null,
|
| 383 |
-
};
|
| 384 |
-
|
| 385 |
-
if (mainJs) {
|
| 386 |
-
console.log(`[extract] main.js loaded (${mainJs.split("\n").length} lines)`);
|
| 387 |
-
|
| 388 |
-
mainJsResults = extractFromMainJs(mainJs, patterns.main_js);
|
| 389 |
-
console.log(` API base URL: ${mainJsResults.apiBaseUrl}`);
|
| 390 |
-
console.log(` originator: ${mainJsResults.originator}`);
|
| 391 |
-
console.log(` models: ${mainJsResults.models.join(", ")}`);
|
| 392 |
-
console.log(` WHAM endpoints: ${mainJsResults.whamEndpoints.length} found`);
|
| 393 |
-
|
| 394 |
-
// Extract system prompts
|
| 395 |
-
console.log("[extract] Extracting system prompts...");
|
| 396 |
-
promptResults = extractPrompts(mainJs);
|
| 397 |
-
console.log(` desktop-context: ${promptResults.desktopContext ? "found" : "NOT FOUND"}`);
|
| 398 |
-
console.log(` title-generation: ${promptResults.titleGeneration ? "found" : "NOT FOUND"}`);
|
| 399 |
-
console.log(` pr-generation: ${promptResults.prGeneration ? "found" : "NOT FOUND"}`);
|
| 400 |
-
console.log(` automation-response: ${promptResults.automationResponse ? "found" : "NOT FOUND"}`);
|
| 401 |
-
}
|
| 402 |
-
|
| 403 |
-
// Save extracted prompts
|
| 404 |
-
const dc = savePrompt("desktop-context", promptResults.desktopContext);
|
| 405 |
-
const tg = savePrompt("title-generation", promptResults.titleGeneration);
|
| 406 |
-
const pr = savePrompt("pr-generation", promptResults.prGeneration);
|
| 407 |
-
const ar = savePrompt("automation-response", promptResults.automationResponse);
|
| 408 |
-
|
| 409 |
-
// Build output
|
| 410 |
-
const fingerprint: ExtractedFingerprint = {
|
| 411 |
-
app_version: version,
|
| 412 |
-
build_number: buildNumber,
|
| 413 |
-
api_base_url: mainJsResults.apiBaseUrl,
|
| 414 |
-
originator: mainJsResults.originator,
|
| 415 |
-
models: mainJsResults.models,
|
| 416 |
-
wham_endpoints: mainJsResults.whamEndpoints,
|
| 417 |
-
user_agent_contains: mainJsResults.userAgentContains,
|
| 418 |
-
sparkle_feed_url: sparkleFeedUrl,
|
| 419 |
-
prompts: {
|
| 420 |
-
desktop_context_hash: dc.hash,
|
| 421 |
-
desktop_context_path: dc.path,
|
| 422 |
-
title_generation_hash: tg.hash,
|
| 423 |
-
title_generation_path: tg.path,
|
| 424 |
-
pr_generation_hash: pr.hash,
|
| 425 |
-
pr_generation_path: pr.path,
|
| 426 |
-
automation_response_hash: ar.hash,
|
| 427 |
-
automation_response_path: ar.path,
|
| 428 |
-
},
|
| 429 |
-
extracted_at: new Date().toISOString(),
|
| 430 |
-
source_path: inputPath,
|
| 431 |
-
};
|
| 432 |
-
|
| 433 |
-
// Write output
|
| 434 |
-
mkdirSync(resolve(ROOT, "data"), { recursive: true });
|
| 435 |
-
writeFileSync(OUTPUT_PATH, JSON.stringify(fingerprint, null, 2));
|
| 436 |
-
|
| 437 |
-
console.log(`\n[extract] Fingerprint written to ${OUTPUT_PATH}`);
|
| 438 |
-
console.log(`[extract] Prompts written to ${PROMPTS_DIR}/`);
|
| 439 |
-
console.log("[extract] Done.");
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
-
main().catch((err) => {
|
| 443 |
-
console.error("[extract] Fatal:", err);
|
| 444 |
-
process.exit(1);
|
| 445 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test-create-env-gh.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
| 1 |
-
import { loadConfig, loadFingerprint } from '../src/config.js';
|
| 2 |
-
import { ProxyClient } from '../src/proxy/client.js';
|
| 3 |
-
import { AuthManager } from '../src/auth/manager.js';
|
| 4 |
-
|
| 5 |
-
async function main() {
|
| 6 |
-
loadConfig(); loadFingerprint();
|
| 7 |
-
const am = new AuthManager();
|
| 8 |
-
const tk = await am.getToken();
|
| 9 |
-
if (!tk) { console.log('Not auth'); return; }
|
| 10 |
-
const c = new ProxyClient(tk, am.getAccountId());
|
| 11 |
-
|
| 12 |
-
// Create environment: github/octocat/Hello-World
|
| 13 |
-
console.log('=== Create env ===');
|
| 14 |
-
const r = await c.post('wham/environments', {
|
| 15 |
-
machine_id: '6993d698c6a48190bcc7ed131c5f34b4',
|
| 16 |
-
repos: ['github/octocat/Hello-World'],
|
| 17 |
-
label: 'proxy-env',
|
| 18 |
-
});
|
| 19 |
-
console.log(r.status, JSON.stringify(r.body).slice(0, 800));
|
| 20 |
-
|
| 21 |
-
if (r.ok) {
|
| 22 |
-
const env = r.body as { environment_id?: string; id?: string };
|
| 23 |
-
const envId = env.environment_id || env.id;
|
| 24 |
-
console.log('Environment ID:', envId);
|
| 25 |
-
|
| 26 |
-
if (envId) {
|
| 27 |
-
// Create task with this environment
|
| 28 |
-
console.log('\n=== Create task with env ===');
|
| 29 |
-
const tr = await c.post('wham/tasks', {
|
| 30 |
-
new_task: {
|
| 31 |
-
branch: 'main',
|
| 32 |
-
environment_id: envId,
|
| 33 |
-
run_environment_in_qa_mode: false,
|
| 34 |
-
},
|
| 35 |
-
input_items: [{ type: 'message', role: 'user', content: [{ content_type: 'text', text: 'Say hello in one sentence.' }] }],
|
| 36 |
-
model: 'gpt-5.1-codex-mini',
|
| 37 |
-
developer_instructions: null,
|
| 38 |
-
base_instructions: null,
|
| 39 |
-
personality: null,
|
| 40 |
-
approval_policy: 'never',
|
| 41 |
-
sandbox: 'workspace-write',
|
| 42 |
-
ephemeral: null,
|
| 43 |
-
});
|
| 44 |
-
console.log(tr.status, JSON.stringify(tr.body).slice(0, 800));
|
| 45 |
-
|
| 46 |
-
const taskBody = tr.body as { task?: { id?: string; current_turn_id?: string } };
|
| 47 |
-
if (tr.ok && taskBody?.task?.id) {
|
| 48 |
-
const taskId = taskBody.task.id;
|
| 49 |
-
const turnId = taskBody.task.current_turn_id;
|
| 50 |
-
console.log('\nPolling...');
|
| 51 |
-
for (let i = 0; i < 60; i++) {
|
| 52 |
-
await new Promise(res => setTimeout(res, 3000));
|
| 53 |
-
const poll = await c.get(
|
| 54 |
-
'wham/tasks/' + encodeURIComponent(taskId) + '/turns/' + encodeURIComponent(turnId!)
|
| 55 |
-
);
|
| 56 |
-
const pd = poll.body as { turn?: { turn_status?: string; output_items?: unknown[]; error?: unknown } };
|
| 57 |
-
const st = pd?.turn?.turn_status;
|
| 58 |
-
console.log(' ' + i + ': ' + st);
|
| 59 |
-
if (st === 'completed' || st === 'failed' || st === 'cancelled') {
|
| 60 |
-
if (pd?.turn?.error) console.log(' Error:', JSON.stringify(pd.turn.error));
|
| 61 |
-
if (pd?.turn?.output_items) console.log(' Output:', JSON.stringify(pd.turn.output_items).slice(0, 1000));
|
| 62 |
-
break;
|
| 63 |
-
}
|
| 64 |
-
}
|
| 65 |
-
}
|
| 66 |
-
}
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
// Also try listing environments now
|
| 70 |
-
console.log('\n=== List environments ===');
|
| 71 |
-
const le = await c.get('wham/environments');
|
| 72 |
-
console.log(le.status, JSON.stringify(le.body).slice(0, 400));
|
| 73 |
-
}
|
| 74 |
-
main().catch(e => console.error(e));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test-create-env-real.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
| 1 |
-
import { loadConfig, loadFingerprint } from '../src/config.js';
|
| 2 |
-
import { ProxyClient } from '../src/proxy/client.js';
|
| 3 |
-
import { AuthManager } from '../src/auth/manager.js';
|
| 4 |
-
import { buildHeadersWithContentType } from '../src/fingerprint/manager.js';
|
| 5 |
-
|
| 6 |
-
async function main() {
|
| 7 |
-
loadConfig(); loadFingerprint();
|
| 8 |
-
const am = new AuthManager();
|
| 9 |
-
const tk = await am.getToken();
|
| 10 |
-
if (!tk) { console.log('Not authenticated'); return; }
|
| 11 |
-
const aid = am.getAccountId();
|
| 12 |
-
const c = new ProxyClient(tk, aid);
|
| 13 |
-
const base = 'https://chatgpt.com/backend-api';
|
| 14 |
-
|
| 15 |
-
const machineId = '6993d698c6a48190bcc7ed131c5f34b4';
|
| 16 |
-
|
| 17 |
-
// Test 1: Create environment with a real public GitHub repo
|
| 18 |
-
console.log('=== Test 1: POST /wham/environments with real repo ===');
|
| 19 |
-
try {
|
| 20 |
-
const r = await c.post('wham/environments', {
|
| 21 |
-
machine_id: machineId,
|
| 22 |
-
repos: [{ provider: 'github', owner: 'octocat', name: 'Hello-World' }],
|
| 23 |
-
label: 'test-env',
|
| 24 |
-
});
|
| 25 |
-
console.log(r.status, JSON.stringify(r.body).slice(0, 600));
|
| 26 |
-
} catch (e: unknown) { console.log('Error:', (e as Error).message); }
|
| 27 |
-
|
| 28 |
-
// Test 2: Different repos format
|
| 29 |
-
console.log('\n=== Test 2: repos as string array ===');
|
| 30 |
-
try {
|
| 31 |
-
const r = await c.post('wham/environments', {
|
| 32 |
-
machine_id: machineId,
|
| 33 |
-
repos: ['octocat/Hello-World'],
|
| 34 |
-
label: 'test-env-2',
|
| 35 |
-
});
|
| 36 |
-
console.log(r.status, JSON.stringify(r.body).slice(0, 600));
|
| 37 |
-
} catch (e: unknown) { console.log('Error:', (e as Error).message); }
|
| 38 |
-
|
| 39 |
-
// Test 3: Minimal environment creation
|
| 40 |
-
console.log('\n=== Test 3: minimal env ===');
|
| 41 |
-
try {
|
| 42 |
-
const r = await c.post('wham/environments', {
|
| 43 |
-
machine_id: machineId,
|
| 44 |
-
});
|
| 45 |
-
console.log(r.status, JSON.stringify(r.body).slice(0, 600));
|
| 46 |
-
} catch (e: unknown) { console.log('Error:', (e as Error).message); }
|
| 47 |
-
|
| 48 |
-
// Test 4: Create environment via different endpoint
|
| 49 |
-
console.log('\n=== Test 4: POST /wham/environments/create ===');
|
| 50 |
-
try {
|
| 51 |
-
const r = await c.post('wham/environments/create', {
|
| 52 |
-
machine_id: machineId,
|
| 53 |
-
repos: ['octocat/Hello-World'],
|
| 54 |
-
});
|
| 55 |
-
console.log(r.status, JSON.stringify(r.body).slice(0, 600));
|
| 56 |
-
} catch (e: unknown) { console.log('Error:', (e as Error).message); }
|
| 57 |
-
|
| 58 |
-
// Test 5: Try the webview approach - look at how the webview creates environments
|
| 59 |
-
// The webview uses POST /wham/tasks with environment embedded
|
| 60 |
-
// Let me try creating a task without environment_id but with DELETE of the key
|
| 61 |
-
console.log('\n=== Test 5: POST /wham/tasks with environment_id deleted (raw fetch) ===');
|
| 62 |
-
try {
|
| 63 |
-
const body: Record<string, unknown> = {
|
| 64 |
-
new_task: {
|
| 65 |
-
branch: 'main',
|
| 66 |
-
run_environment_in_qa_mode: false,
|
| 67 |
-
},
|
| 68 |
-
input_items: [{ type: 'message', role: 'user', content: [{ content_type: 'text', text: 'Say hello' }] }],
|
| 69 |
-
model: 'gpt-5.1-codex-mini',
|
| 70 |
-
developer_instructions: null,
|
| 71 |
-
base_instructions: null,
|
| 72 |
-
personality: null,
|
| 73 |
-
approval_policy: 'never',
|
| 74 |
-
sandbox: 'workspace-write',
|
| 75 |
-
ephemeral: null,
|
| 76 |
-
};
|
| 77 |
-
// Verify environment_id is truly not in the JSON
|
| 78 |
-
console.log('Body keys:', Object.keys(body.new_task as object));
|
| 79 |
-
const res = await fetch(`${base}/wham/tasks`, {
|
| 80 |
-
method: 'POST',
|
| 81 |
-
headers: buildHeadersWithContentType(tk, aid),
|
| 82 |
-
body: JSON.stringify(body),
|
| 83 |
-
});
|
| 84 |
-
const data = await res.json();
|
| 85 |
-
console.log(res.status, JSON.stringify(data).slice(0, 600));
|
| 86 |
-
} catch (e: unknown) { console.log('Error:', (e as Error).message); }
|
| 87 |
-
|
| 88 |
-
// Test 6: What about follow_up without new_task?
|
| 89 |
-
console.log('\n=== Test 6: POST /wham/tasks without new_task (just input_items) ===');
|
| 90 |
-
try {
|
| 91 |
-
const body = {
|
| 92 |
-
input_items: [{ type: 'message', role: 'user', content: [{ content_type: 'text', text: 'Say hello' }] }],
|
| 93 |
-
model: 'gpt-5.1-codex-mini',
|
| 94 |
-
};
|
| 95 |
-
const res = await fetch(`${base}/wham/tasks`, {
|
| 96 |
-
method: 'POST',
|
| 97 |
-
headers: buildHeadersWithContentType(tk, aid),
|
| 98 |
-
body: JSON.stringify(body),
|
| 99 |
-
});
|
| 100 |
-
const data = await res.json();
|
| 101 |
-
console.log(res.status, JSON.stringify(data).slice(0, 600));
|
| 102 |
-
} catch (e: unknown) { console.log('Error:', (e as Error).message); }
|
| 103 |
-
|
| 104 |
-
// Test 7: Maybe the issue is that we need to send a requestBody wrapper?
|
| 105 |
-
// The webview uses: CodexRequest.safePost("/wham/tasks", { requestBody: {...} })
|
| 106 |
-
console.log('\n=== Test 7: POST /wham/tasks with requestBody wrapper ===');
|
| 107 |
-
try {
|
| 108 |
-
const body = {
|
| 109 |
-
requestBody: {
|
| 110 |
-
new_task: {
|
| 111 |
-
branch: 'main',
|
| 112 |
-
run_environment_in_qa_mode: false,
|
| 113 |
-
},
|
| 114 |
-
input_items: [{ type: 'message', role: 'user', content: [{ content_type: 'text', text: 'Say hello' }] }],
|
| 115 |
-
model: 'gpt-5.1-codex-mini',
|
| 116 |
-
},
|
| 117 |
-
};
|
| 118 |
-
const res = await fetch(`${base}/wham/tasks`, {
|
| 119 |
-
method: 'POST',
|
| 120 |
-
headers: buildHeadersWithContentType(tk, aid),
|
| 121 |
-
body: JSON.stringify(body),
|
| 122 |
-
});
|
| 123 |
-
const data = await res.json();
|
| 124 |
-
console.log(res.status, JSON.stringify(data).slice(0, 600));
|
| 125 |
-
} catch (e: unknown) { console.log('Error:', (e as Error).message); }
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
main().catch(e => console.error(e));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test-env-full.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
| 1 |
-
import { loadConfig, loadFingerprint } from '../src/config.js';
|
| 2 |
-
import { ProxyClient } from '../src/proxy/client.js';
|
| 3 |
-
import { AuthManager } from '../src/auth/manager.js';
|
| 4 |
-
import { buildHeaders, buildHeadersWithContentType } from '../src/fingerprint/manager.js';
|
| 5 |
-
|
| 6 |
-
async function main() {
|
| 7 |
-
loadConfig(); loadFingerprint();
|
| 8 |
-
const am = new AuthManager();
|
| 9 |
-
const tk = await am.getToken();
|
| 10 |
-
if (!tk) { console.log('Not authenticated'); return; }
|
| 11 |
-
const aid = am.getAccountId();
|
| 12 |
-
const c = new ProxyClient(tk, aid);
|
| 13 |
-
const base = 'https://chatgpt.com/backend-api';
|
| 14 |
-
|
| 15 |
-
// Try all possible environment-related endpoints
|
| 16 |
-
const endpoints = [
|
| 17 |
-
{ method: 'GET', path: 'wham/environments' },
|
| 18 |
-
{ method: 'GET', path: 'wham/machines' },
|
| 19 |
-
{ method: 'GET', path: 'wham/accounts/check' },
|
| 20 |
-
{ method: 'GET', path: 'wham/usage' },
|
| 21 |
-
{ method: 'GET', path: 'wham/tasks' },
|
| 22 |
-
// Codex CLI endpoints
|
| 23 |
-
{ method: 'GET', path: 'api/codex/environments' },
|
| 24 |
-
// Try to see if there's a way to check features
|
| 25 |
-
{ method: 'GET', path: 'wham/features' },
|
| 26 |
-
{ method: 'GET', path: 'wham/config' },
|
| 27 |
-
// Try without environment at all — use ephemeral
|
| 28 |
-
{ method: 'POST', path: 'wham/tasks', body: {
|
| 29 |
-
new_task: { branch: 'main', environment_id: null, run_environment_in_qa_mode: false },
|
| 30 |
-
input_items: [{ type: 'message', role: 'user', content: [{ content_type: 'text', text: 'Say hi' }] }],
|
| 31 |
-
model: 'gpt-5.1-codex-mini',
|
| 32 |
-
ephemeral: true,
|
| 33 |
-
}},
|
| 34 |
-
// Try with ephemeral flag and no environment
|
| 35 |
-
{ method: 'POST', path: 'wham/tasks', body: {
|
| 36 |
-
new_task: { branch: 'main' },
|
| 37 |
-
input_items: [{ type: 'message', role: 'user', content: [{ content_type: 'text', text: 'Say hi' }] }],
|
| 38 |
-
model: 'gpt-5.1-codex-mini',
|
| 39 |
-
ephemeral: true,
|
| 40 |
-
approval_policy: 'never',
|
| 41 |
-
sandbox: 'workspace-write',
|
| 42 |
-
}},
|
| 43 |
-
];
|
| 44 |
-
|
| 45 |
-
for (const ep of endpoints) {
|
| 46 |
-
console.log(`\n=== ${ep.method} /${ep.path} ===`);
|
| 47 |
-
try {
|
| 48 |
-
let res: Response;
|
| 49 |
-
if (ep.method === 'POST') {
|
| 50 |
-
res = await fetch(`${base}/${ep.path}`, {
|
| 51 |
-
method: 'POST',
|
| 52 |
-
headers: buildHeadersWithContentType(tk, aid),
|
| 53 |
-
body: JSON.stringify(ep.body),
|
| 54 |
-
});
|
| 55 |
-
} else {
|
| 56 |
-
res = await fetch(`${base}/${ep.path}`, {
|
| 57 |
-
headers: buildHeaders(tk, aid),
|
| 58 |
-
});
|
| 59 |
-
}
|
| 60 |
-
const ct = res.headers.get('content-type') || '';
|
| 61 |
-
if (ct.includes('json')) {
|
| 62 |
-
const data = await res.json();
|
| 63 |
-
console.log(res.status, JSON.stringify(data).slice(0, 600));
|
| 64 |
-
} else {
|
| 65 |
-
const text = await res.text();
|
| 66 |
-
console.log(res.status, text.slice(0, 300));
|
| 67 |
-
}
|
| 68 |
-
} catch (e: unknown) {
|
| 69 |
-
console.log('Error:', (e as Error).message);
|
| 70 |
-
}
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
// Also try listing existing tasks to see if any have environment info
|
| 74 |
-
console.log('\n=== GET /wham/tasks (list existing tasks) ===');
|
| 75 |
-
try {
|
| 76 |
-
const r = await fetch(`${base}/wham/tasks`, {
|
| 77 |
-
headers: buildHeaders(tk, aid),
|
| 78 |
-
});
|
| 79 |
-
const data = await r.json() as { items?: Array<{ id?: string; task_status_display?: unknown }> };
|
| 80 |
-
console.log(r.status);
|
| 81 |
-
if (data.items) {
|
| 82 |
-
console.log('Total tasks:', data.items.length);
|
| 83 |
-
for (const t of data.items.slice(0, 3)) {
|
| 84 |
-
console.log(' Task:', t.id, JSON.stringify(t.task_status_display).slice(0, 200));
|
| 85 |
-
}
|
| 86 |
-
} else {
|
| 87 |
-
console.log(JSON.stringify(data).slice(0, 400));
|
| 88 |
-
}
|
| 89 |
-
} catch (e: unknown) {
|
| 90 |
-
console.log('Error:', (e as Error).message);
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
// Check a specific task to see its environment info
|
| 94 |
-
console.log('\n=== Check a recent task for environment details ===');
|
| 95 |
-
try {
|
| 96 |
-
const r = await fetch(`${base}/wham/tasks`, {
|
| 97 |
-
headers: buildHeaders(tk, aid),
|
| 98 |
-
});
|
| 99 |
-
const data = await r.json() as { items?: Array<{ id?: string }> };
|
| 100 |
-
if (data.items && data.items.length > 0) {
|
| 101 |
-
const taskId = data.items[0].id!;
|
| 102 |
-
const tr = await fetch(`${base}/wham/tasks/${encodeURIComponent(taskId)}`, {
|
| 103 |
-
headers: buildHeaders(tk, aid),
|
| 104 |
-
});
|
| 105 |
-
const td = await tr.json();
|
| 106 |
-
console.log('Task detail:', JSON.stringify(td).slice(0, 1000));
|
| 107 |
-
}
|
| 108 |
-
} catch (e: unknown) {
|
| 109 |
-
console.log('Error:', (e as Error).message);
|
| 110 |
-
}
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
main().catch(e => console.error(e));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test-repo-format.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
import { loadConfig, loadFingerprint } from '../src/config.js';
|
| 2 |
-
import { ProxyClient } from '../src/proxy/client.js';
|
| 3 |
-
import { AuthManager } from '../src/auth/manager.js';
|
| 4 |
-
|
| 5 |
-
async function main() {
|
| 6 |
-
loadConfig(); loadFingerprint();
|
| 7 |
-
const am = new AuthManager();
|
| 8 |
-
const tk = await am.getToken();
|
| 9 |
-
if (!tk) { console.log('Not auth'); return; }
|
| 10 |
-
const c = new ProxyClient(tk, am.getAccountId());
|
| 11 |
-
const mid = '6993d698c6a48190bcc7ed131c5f34b4';
|
| 12 |
-
|
| 13 |
-
const formats = [
|
| 14 |
-
'github:octocat/Hello-World',
|
| 15 |
-
'octocat/Hello-World',
|
| 16 |
-
'github/octocat/hello-world',
|
| 17 |
-
'https://github.com/octocat/Hello-World',
|
| 18 |
-
];
|
| 19 |
-
|
| 20 |
-
for (const repo of formats) {
|
| 21 |
-
console.log('--- repos: ["' + repo + '"] ---');
|
| 22 |
-
const r = await c.post('wham/environments', { machine_id: mid, repos: [repo], label: 'test' });
|
| 23 |
-
console.log(r.status, JSON.stringify(r.body).slice(0, 300));
|
| 24 |
-
console.log('');
|
| 25 |
-
}
|
| 26 |
-
}
|
| 27 |
-
main().catch(e => console.error(e));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test-repo2.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
import { loadConfig, loadFingerprint } from '../src/config.js';
|
| 2 |
-
import { ProxyClient } from '../src/proxy/client.js';
|
| 3 |
-
import { AuthManager } from '../src/auth/manager.js';
|
| 4 |
-
async function main() {
|
| 5 |
-
loadConfig(); loadFingerprint();
|
| 6 |
-
const am = new AuthManager();
|
| 7 |
-
const tk = await am.getToken();
|
| 8 |
-
if (!tk) { console.log('no auth'); return; }
|
| 9 |
-
const c = new ProxyClient(tk, am.getAccountId());
|
| 10 |
-
const mid = '6993d698c6a48190bcc7ed131c5f34b4';
|
| 11 |
-
const tests = [
|
| 12 |
-
'github/torvalds/linux',
|
| 13 |
-
'github/facebook/react',
|
| 14 |
-
'torvalds/linux',
|
| 15 |
-
];
|
| 16 |
-
for (const repo of tests) {
|
| 17 |
-
console.log('repos: [' + repo + ']');
|
| 18 |
-
const r = await c.post('wham/environments', { machine_id: mid, repos: [repo], label: 'test' });
|
| 19 |
-
console.log(r.status, JSON.stringify(r.body).slice(0, 400));
|
| 20 |
-
console.log('');
|
| 21 |
-
}
|
| 22 |
-
}
|
| 23 |
-
main();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test-responses-api.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
| 1 |
-
import { loadConfig, loadFingerprint } from '../src/config.js';
|
| 2 |
-
import { AuthManager } from '../src/auth/manager.js';
|
| 3 |
-
import { buildHeadersWithContentType } from '../src/fingerprint/manager.js';
|
| 4 |
-
|
| 5 |
-
async function main() {
|
| 6 |
-
loadConfig(); loadFingerprint();
|
| 7 |
-
const am = new AuthManager();
|
| 8 |
-
const tk = await am.getToken();
|
| 9 |
-
if (!tk) { console.log('no auth token'); return; }
|
| 10 |
-
const aid = am.getAccountId();
|
| 11 |
-
|
| 12 |
-
const base = 'https://chatgpt.com/backend-api/codex';
|
| 13 |
-
const hdrs = buildHeadersWithContentType(tk, aid);
|
| 14 |
-
|
| 15 |
-
// Test 1: non-streaming
|
| 16 |
-
console.log('=== Test 1: POST /codex/responses (non-streaming) ===');
|
| 17 |
-
try {
|
| 18 |
-
const r = await fetch(base + '/responses', {
|
| 19 |
-
method: 'POST',
|
| 20 |
-
headers: hdrs,
|
| 21 |
-
body: JSON.stringify({
|
| 22 |
-
model: 'gpt-5.1-codex-mini',
|
| 23 |
-
instructions: 'You are a helpful assistant.',
|
| 24 |
-
input: [{ role: 'user', content: 'Say hello in one sentence.' }],
|
| 25 |
-
stream: false,
|
| 26 |
-
store: false,
|
| 27 |
-
}),
|
| 28 |
-
});
|
| 29 |
-
console.log('Status:', r.status, r.statusText);
|
| 30 |
-
const ct = r.headers.get('content-type') || '';
|
| 31 |
-
console.log('Content-Type:', ct);
|
| 32 |
-
if (ct.includes('json')) {
|
| 33 |
-
const j = await r.json();
|
| 34 |
-
console.log('Body:', JSON.stringify(j, null, 2).slice(0, 3000));
|
| 35 |
-
} else {
|
| 36 |
-
const t = await r.text();
|
| 37 |
-
console.log('Body:', t.slice(0, 2000));
|
| 38 |
-
}
|
| 39 |
-
} catch (e: any) {
|
| 40 |
-
console.log('Error:', e.message);
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
// Test 2: streaming
|
| 44 |
-
console.log('\n=== Test 2: POST /codex/responses (streaming) ===');
|
| 45 |
-
try {
|
| 46 |
-
const streamHdrs = { ...hdrs, 'Accept': 'text/event-stream' };
|
| 47 |
-
const r = await fetch(base + '/responses', {
|
| 48 |
-
method: 'POST',
|
| 49 |
-
headers: streamHdrs,
|
| 50 |
-
body: JSON.stringify({
|
| 51 |
-
model: 'gpt-5.1-codex-mini',
|
| 52 |
-
instructions: 'You are a helpful assistant.',
|
| 53 |
-
input: [{ role: 'user', content: 'Say hello in one sentence.' }],
|
| 54 |
-
stream: true,
|
| 55 |
-
store: false,
|
| 56 |
-
}),
|
| 57 |
-
});
|
| 58 |
-
console.log('Status:', r.status, r.statusText);
|
| 59 |
-
const ct = r.headers.get('content-type') || '';
|
| 60 |
-
console.log('Content-Type:', ct);
|
| 61 |
-
|
| 62 |
-
if (r.status !== 200) {
|
| 63 |
-
const t = await r.text();
|
| 64 |
-
console.log('Error body:', t.slice(0, 1000));
|
| 65 |
-
} else {
|
| 66 |
-
const reader = r.body!.getReader();
|
| 67 |
-
const decoder = new TextDecoder();
|
| 68 |
-
let fullText = '';
|
| 69 |
-
let chunks = 0;
|
| 70 |
-
while (true) {
|
| 71 |
-
const { done, value } = await reader.read();
|
| 72 |
-
if (done) break;
|
| 73 |
-
const chunk = decoder.decode(value, { stream: true });
|
| 74 |
-
fullText += chunk;
|
| 75 |
-
chunks++;
|
| 76 |
-
if (chunks <= 10) {
|
| 77 |
-
console.log(`--- Chunk ${chunks} ---`);
|
| 78 |
-
console.log(chunk.slice(0, 300));
|
| 79 |
-
}
|
| 80 |
-
}
|
| 81 |
-
console.log('\nTotal chunks:', chunks);
|
| 82 |
-
console.log('Full response length:', fullText.length);
|
| 83 |
-
// Show some events
|
| 84 |
-
const events = fullText.split('\n\n').filter(e => e.trim());
|
| 85 |
-
console.log('Total SSE events:', events.length);
|
| 86 |
-
// Show last few events
|
| 87 |
-
const last5 = events.slice(-5);
|
| 88 |
-
console.log('\nLast 5 events:');
|
| 89 |
-
for (const ev of last5) {
|
| 90 |
-
console.log(ev.slice(0, 300));
|
| 91 |
-
console.log('---');
|
| 92 |
-
}
|
| 93 |
-
}
|
| 94 |
-
} catch (e: any) {
|
| 95 |
-
console.log('Error:', e.message);
|
| 96 |
-
}
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
main();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/test-snapshot.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
| 1 |
-
import { loadConfig, loadFingerprint } from '../src/config.js';
|
| 2 |
-
import { ProxyClient } from '../src/proxy/client.js';
|
| 3 |
-
import { AuthManager } from '../src/auth/manager.js';
|
| 4 |
-
|
| 5 |
-
async function main() {
|
| 6 |
-
loadConfig(); loadFingerprint();
|
| 7 |
-
const am = new AuthManager();
|
| 8 |
-
const tk = await am.getToken();
|
| 9 |
-
if (!tk) { console.log('Not authenticated'); return; }
|
| 10 |
-
const c = new ProxyClient(tk, am.getAccountId());
|
| 11 |
-
|
| 12 |
-
// Step 1: Get upload URL for a worktree snapshot
|
| 13 |
-
console.log('=== Step 1: POST /wham/worktree_snapshots/upload_url ===');
|
| 14 |
-
const r1 = await c.post('wham/worktree_snapshots/upload_url', {
|
| 15 |
-
file_name: 'snapshot.tar.gz',
|
| 16 |
-
file_size: 100,
|
| 17 |
-
repo_name: 'workspace',
|
| 18 |
-
});
|
| 19 |
-
console.log('Status:', r1.status);
|
| 20 |
-
console.log('Body:', JSON.stringify(r1.body).slice(0, 800));
|
| 21 |
-
|
| 22 |
-
if (!r1.ok) {
|
| 23 |
-
console.log('Failed to get upload URL');
|
| 24 |
-
return;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
const body1 = r1.body as { upload_url?: string; file_id?: string; status?: string };
|
| 28 |
-
console.log('Upload URL:', body1.upload_url?.slice(0, 100));
|
| 29 |
-
console.log('File ID:', body1.file_id);
|
| 30 |
-
|
| 31 |
-
// Step 2: Upload a minimal tar.gz (empty)
|
| 32 |
-
if (body1.upload_url) {
|
| 33 |
-
console.log('\n=== Step 2: Upload minimal snapshot ===');
|
| 34 |
-
// Create a minimal gzip file (empty gzip)
|
| 35 |
-
const emptyGzip = Buffer.from([
|
| 36 |
-
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
|
| 37 |
-
0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
| 38 |
-
0x00, 0x00, 0x00, 0x00,
|
| 39 |
-
]);
|
| 40 |
-
|
| 41 |
-
const uploadRes = await fetch(body1.upload_url, {
|
| 42 |
-
method: 'PUT',
|
| 43 |
-
body: emptyGzip,
|
| 44 |
-
headers: { 'Content-Type': 'application/gzip' },
|
| 45 |
-
});
|
| 46 |
-
console.log('Upload status:', uploadRes.status);
|
| 47 |
-
const uploadText = await uploadRes.text();
|
| 48 |
-
console.log('Upload response:', uploadText.slice(0, 300));
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
// Step 3: Finish upload
|
| 52 |
-
if (body1.file_id) {
|
| 53 |
-
console.log('\n=== Step 3: POST /wham/worktree_snapshots/finish_upload ===');
|
| 54 |
-
const r3 = await c.post('wham/worktree_snapshots/finish_upload', {
|
| 55 |
-
file_id: body1.file_id,
|
| 56 |
-
});
|
| 57 |
-
console.log('Status:', r3.status);
|
| 58 |
-
console.log('Body:', JSON.stringify(r3.body).slice(0, 800));
|
| 59 |
-
|
| 60 |
-
// Step 4: Create a task with this file_id
|
| 61 |
-
console.log('\n=== Step 4: Create task with valid file_id ===');
|
| 62 |
-
const r4 = await c.post('wham/tasks', {
|
| 63 |
-
new_task: {
|
| 64 |
-
branch: 'main',
|
| 65 |
-
environment_id: null,
|
| 66 |
-
environment: {
|
| 67 |
-
repos: [{
|
| 68 |
-
kind: 'local_worktree',
|
| 69 |
-
name: 'workspace',
|
| 70 |
-
remotes: {},
|
| 71 |
-
commit_sha: '0000000000000000000000000000000000000000',
|
| 72 |
-
branch: 'main',
|
| 73 |
-
file_id: body1.file_id,
|
| 74 |
-
}],
|
| 75 |
-
},
|
| 76 |
-
run_environment_in_qa_mode: false,
|
| 77 |
-
},
|
| 78 |
-
input_items: [
|
| 79 |
-
{ type: 'message', role: 'user', content: [{ content_type: 'text', text: 'Say hello in one sentence.' }] }
|
| 80 |
-
],
|
| 81 |
-
model: 'gpt-5.1-codex-mini',
|
| 82 |
-
developer_instructions: null,
|
| 83 |
-
base_instructions: null,
|
| 84 |
-
personality: null,
|
| 85 |
-
approval_policy: 'never',
|
| 86 |
-
sandbox: 'workspace-write',
|
| 87 |
-
ephemeral: null,
|
| 88 |
-
});
|
| 89 |
-
console.log('Task status:', r4.status);
|
| 90 |
-
console.log('Task body:', JSON.stringify(r4.body).slice(0, 600));
|
| 91 |
-
|
| 92 |
-
// Step 5: Poll for result
|
| 93 |
-
const taskBody = r4.body as { task?: { id?: string; current_turn_id?: string } };
|
| 94 |
-
if (r4.ok && taskBody?.task?.id) {
|
| 95 |
-
const taskId = taskBody.task.id;
|
| 96 |
-
const turnId = taskBody.task.current_turn_id!;
|
| 97 |
-
console.log('\n=== Step 5: Poll for turn completion ===');
|
| 98 |
-
console.log('Task:', taskId);
|
| 99 |
-
console.log('Turn:', turnId);
|
| 100 |
-
|
| 101 |
-
for (let i = 0; i < 30; i++) {
|
| 102 |
-
await new Promise(res => setTimeout(res, 3000));
|
| 103 |
-
const tr = await c.get(`wham/tasks/${encodeURIComponent(taskId)}/turns/${encodeURIComponent(turnId)}`);
|
| 104 |
-
const td = tr.body as { turn?: { turn_status?: string; output_items?: unknown[]; error?: unknown } };
|
| 105 |
-
const status = td?.turn?.turn_status;
|
| 106 |
-
console.log(` Poll ${i}: status=${status}`);
|
| 107 |
-
if (status === 'completed' || status === 'failed' || status === 'cancelled') {
|
| 108 |
-
if (td?.turn?.error) console.log(' Error:', JSON.stringify(td.turn.error));
|
| 109 |
-
if (td?.turn?.output_items) console.log(' Output:', JSON.stringify(td.turn.output_items).slice(0, 500));
|
| 110 |
-
break;
|
| 111 |
-
}
|
| 112 |
-
}
|
| 113 |
-
}
|
| 114 |
-
}
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
main().catch(e => console.error(e));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/auth/oauth-pkce.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { readFileSync, existsSync } from "fs";
|
|
| 9 |
import { resolve } from "path";
|
| 10 |
import { homedir } from "os";
|
| 11 |
import { getConfig } from "../config.js";
|
|
|
|
| 12 |
|
| 13 |
export interface PKCEChallenge {
|
| 14 |
codeVerifier: string;
|
|
@@ -120,18 +121,17 @@ export async function exchangeCode(
|
|
| 120 |
code_verifier: codeVerifier,
|
| 121 |
});
|
| 122 |
|
| 123 |
-
const resp = await
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
body
|
| 127 |
-
|
| 128 |
|
| 129 |
if (!resp.ok) {
|
| 130 |
-
|
| 131 |
-
throw new Error(`Token exchange failed (${resp.status}): ${text}`);
|
| 132 |
}
|
| 133 |
|
| 134 |
-
return
|
| 135 |
}
|
| 136 |
|
| 137 |
/**
|
|
@@ -148,18 +148,17 @@ export async function refreshAccessToken(
|
|
| 148 |
refresh_token: refreshToken,
|
| 149 |
});
|
| 150 |
|
| 151 |
-
const resp = await
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
body
|
| 155 |
-
|
| 156 |
|
| 157 |
if (!resp.ok) {
|
| 158 |
-
|
| 159 |
-
throw new Error(`Token refresh failed (${resp.status}): ${text}`);
|
| 160 |
}
|
| 161 |
|
| 162 |
-
return
|
| 163 |
}
|
| 164 |
|
| 165 |
// ── Pending session management ─────────────────────────────────────
|
|
@@ -343,18 +342,17 @@ export async function requestDeviceCode(): Promise<DeviceCodeResponse> {
|
|
| 343 |
scope: "openid profile email offline_access",
|
| 344 |
});
|
| 345 |
|
| 346 |
-
const resp = await
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
body
|
| 350 |
-
|
| 351 |
|
| 352 |
if (!resp.ok) {
|
| 353 |
-
|
| 354 |
-
throw new Error(`Device code request failed (${resp.status}): ${text}`);
|
| 355 |
}
|
| 356 |
|
| 357 |
-
return
|
| 358 |
}
|
| 359 |
|
| 360 |
/**
|
|
@@ -370,20 +368,20 @@ export async function pollDeviceToken(deviceCode: string): Promise<TokenResponse
|
|
| 370 |
client_id: config.auth.oauth_client_id,
|
| 371 |
});
|
| 372 |
|
| 373 |
-
const resp = await
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
body
|
| 377 |
-
|
| 378 |
|
| 379 |
if (!resp.ok) {
|
| 380 |
-
const data = (
|
| 381 |
const err = new Error(data.error_description || data.error || `Poll failed (${resp.status})`);
|
| 382 |
(err as any).code = data.error;
|
| 383 |
throw err;
|
| 384 |
}
|
| 385 |
|
| 386 |
-
return
|
| 387 |
}
|
| 388 |
|
| 389 |
// ── CLI Token Import ───────────────────────────────────────────────
|
|
|
|
| 9 |
import { resolve } from "path";
|
| 10 |
import { homedir } from "os";
|
| 11 |
import { getConfig } from "../config.js";
|
| 12 |
+
import { curlFetchPost } from "../tls/curl-fetch.js";
|
| 13 |
|
| 14 |
export interface PKCEChallenge {
|
| 15 |
codeVerifier: string;
|
|
|
|
| 121 |
code_verifier: codeVerifier,
|
| 122 |
});
|
| 123 |
|
| 124 |
+
const resp = await curlFetchPost(
|
| 125 |
+
config.auth.oauth_token_endpoint,
|
| 126 |
+
"application/x-www-form-urlencoded",
|
| 127 |
+
body.toString(),
|
| 128 |
+
);
|
| 129 |
|
| 130 |
if (!resp.ok) {
|
| 131 |
+
throw new Error(`Token exchange failed (${resp.status}): ${resp.body}`);
|
|
|
|
| 132 |
}
|
| 133 |
|
| 134 |
+
return JSON.parse(resp.body) as TokenResponse;
|
| 135 |
}
|
| 136 |
|
| 137 |
/**
|
|
|
|
| 148 |
refresh_token: refreshToken,
|
| 149 |
});
|
| 150 |
|
| 151 |
+
const resp = await curlFetchPost(
|
| 152 |
+
config.auth.oauth_token_endpoint,
|
| 153 |
+
"application/x-www-form-urlencoded",
|
| 154 |
+
body.toString(),
|
| 155 |
+
);
|
| 156 |
|
| 157 |
if (!resp.ok) {
|
| 158 |
+
throw new Error(`Token refresh failed (${resp.status}): ${resp.body}`);
|
|
|
|
| 159 |
}
|
| 160 |
|
| 161 |
+
return JSON.parse(resp.body) as TokenResponse;
|
| 162 |
}
|
| 163 |
|
| 164 |
// ── Pending session management ─────────────────────────────────────
|
|
|
|
| 342 |
scope: "openid profile email offline_access",
|
| 343 |
});
|
| 344 |
|
| 345 |
+
const resp = await curlFetchPost(
|
| 346 |
+
"https://auth.openai.com/oauth/device/code",
|
| 347 |
+
"application/x-www-form-urlencoded",
|
| 348 |
+
body.toString(),
|
| 349 |
+
);
|
| 350 |
|
| 351 |
if (!resp.ok) {
|
| 352 |
+
throw new Error(`Device code request failed (${resp.status}): ${resp.body}`);
|
|
|
|
| 353 |
}
|
| 354 |
|
| 355 |
+
return JSON.parse(resp.body) as DeviceCodeResponse;
|
| 356 |
}
|
| 357 |
|
| 358 |
/**
|
|
|
|
| 368 |
client_id: config.auth.oauth_client_id,
|
| 369 |
});
|
| 370 |
|
| 371 |
+
const resp = await curlFetchPost(
|
| 372 |
+
config.auth.oauth_token_endpoint,
|
| 373 |
+
"application/x-www-form-urlencoded",
|
| 374 |
+
body.toString(),
|
| 375 |
+
);
|
| 376 |
|
| 377 |
if (!resp.ok) {
|
| 378 |
+
const data = JSON.parse(resp.body) as { error?: string; error_description?: string };
|
| 379 |
const err = new Error(data.error_description || data.error || `Poll failed (${resp.status})`);
|
| 380 |
(err as any).code = data.error;
|
| 381 |
throw err;
|
| 382 |
}
|
| 383 |
|
| 384 |
+
return JSON.parse(resp.body) as TokenResponse;
|
| 385 |
}
|
| 386 |
|
| 387 |
// ── CLI Token Import ───────────────────────────────────────────────
|
src/config.ts
CHANGED
|
@@ -42,6 +42,10 @@ const ConfigSchema = z.object({
|
|
| 42 |
ttl_minutes: z.number().default(60),
|
| 43 |
cleanup_interval_minutes: z.number().default(5),
|
| 44 |
}),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
streaming: z.object({
|
| 46 |
status_as_content: z.boolean().default(false),
|
| 47 |
chunk_size: z.number().default(100),
|
|
|
|
| 42 |
ttl_minutes: z.number().default(60),
|
| 43 |
cleanup_interval_minutes: z.number().default(5),
|
| 44 |
}),
|
| 45 |
+
tls: z.object({
|
| 46 |
+
curl_binary: z.string().default("auto"),
|
| 47 |
+
impersonate_profile: z.string().default("chrome136"),
|
| 48 |
+
}).default({}),
|
| 49 |
streaming: z.object({
|
| 50 |
status_as_content: z.boolean().default(false),
|
| 51 |
chunk_size: z.number().default(100),
|
src/proxy/codex-api.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
| 11 |
|
| 12 |
import { spawn, execFile } from "child_process";
|
| 13 |
import { getConfig } from "../config.js";
|
|
|
|
| 14 |
import {
|
| 15 |
buildHeaders,
|
| 16 |
buildHeadersWithContentType,
|
|
@@ -107,11 +108,11 @@ export class CodexApi {
|
|
| 107 |
): Promise<CurlResponse> {
|
| 108 |
return new Promise((resolve, reject) => {
|
| 109 |
const args = [
|
|
|
|
| 110 |
"-s", "-S", // silent but show errors
|
| 111 |
"--compressed", // curl negotiates compression
|
| 112 |
"-N", // no output buffering (SSE)
|
| 113 |
"-i", // include response headers in stdout
|
| 114 |
-
"--http1.1", // force HTTP/1.1
|
| 115 |
"-X", "POST",
|
| 116 |
"--data-binary", "@-", // read body from stdin
|
| 117 |
];
|
|
@@ -120,16 +121,17 @@ export class CodexApi {
|
|
| 120 |
args.push("--max-time", String(timeoutSec));
|
| 121 |
}
|
| 122 |
|
| 123 |
-
//
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
for (const [key, value] of Object.entries(filteredHeaders)) {
|
| 128 |
args.push("-H", `${key}: ${value}`);
|
| 129 |
}
|
|
|
|
|
|
|
| 130 |
args.push(url);
|
| 131 |
|
| 132 |
-
const child = spawn(
|
| 133 |
stdio: ["pipe", "pipe", "pipe"],
|
| 134 |
});
|
| 135 |
|
|
@@ -244,15 +246,15 @@ export class CodexApi {
|
|
| 244 |
// to fail when it can't decompress the response.
|
| 245 |
delete headers["Accept-Encoding"];
|
| 246 |
|
| 247 |
-
// Build curl args
|
| 248 |
-
const args = ["-s", "--compressed", "--max-time", "15"];
|
| 249 |
for (const [key, value] of Object.entries(headers)) {
|
| 250 |
args.push("-H", `${key}: ${value}`);
|
| 251 |
}
|
| 252 |
args.push(url);
|
| 253 |
|
| 254 |
const body = await new Promise<string>((resolve, reject) => {
|
| 255 |
-
execFile(
|
| 256 |
if (err) {
|
| 257 |
reject(new CodexApiError(0, `curl failed: ${err.message} ${stderr}`));
|
| 258 |
} else {
|
|
|
|
| 11 |
|
| 12 |
import { spawn, execFile } from "child_process";
|
| 13 |
import { getConfig } from "../config.js";
|
| 14 |
+
import { resolveCurlBinary, getChromeTlsArgs } from "../tls/curl-binary.js";
|
| 15 |
import {
|
| 16 |
buildHeaders,
|
| 17 |
buildHeadersWithContentType,
|
|
|
|
| 108 |
): Promise<CurlResponse> {
|
| 109 |
return new Promise((resolve, reject) => {
|
| 110 |
const args = [
|
| 111 |
+
...getChromeTlsArgs(), // Chrome TLS profile (ciphers, HTTP/2, etc.)
|
| 112 |
"-s", "-S", // silent but show errors
|
| 113 |
"--compressed", // curl negotiates compression
|
| 114 |
"-N", // no output buffering (SSE)
|
| 115 |
"-i", // include response headers in stdout
|
|
|
|
| 116 |
"-X", "POST",
|
| 117 |
"--data-binary", "@-", // read body from stdin
|
| 118 |
];
|
|
|
|
| 121 |
args.push("--max-time", String(timeoutSec));
|
| 122 |
}
|
| 123 |
|
| 124 |
+
// Pass all headers explicitly in our fingerprint order.
|
| 125 |
+
// Accept-Encoding is kept so curl doesn't inject its own at position 2.
|
| 126 |
+
// --compressed still handles auto-decompression of the response.
|
| 127 |
+
for (const [key, value] of Object.entries(headers)) {
|
|
|
|
| 128 |
args.push("-H", `${key}: ${value}`);
|
| 129 |
}
|
| 130 |
+
// Suppress curl's auto Expect: 100-continue (Chromium never sends it)
|
| 131 |
+
args.push("-H", "Expect:");
|
| 132 |
args.push(url);
|
| 133 |
|
| 134 |
+
const child = spawn(resolveCurlBinary(), args, {
|
| 135 |
stdio: ["pipe", "pipe", "pipe"],
|
| 136 |
});
|
| 137 |
|
|
|
|
| 246 |
// to fail when it can't decompress the response.
|
| 247 |
delete headers["Accept-Encoding"];
|
| 248 |
|
| 249 |
+
// Build curl args (Chrome TLS profile + request params)
|
| 250 |
+
const args = [...getChromeTlsArgs(), "-s", "--compressed", "--max-time", "15"];
|
| 251 |
for (const [key, value] of Object.entries(headers)) {
|
| 252 |
args.push("-H", `${key}: ${value}`);
|
| 253 |
}
|
| 254 |
args.push(url);
|
| 255 |
|
| 256 |
const body = await new Promise<string>((resolve, reject) => {
|
| 257 |
+
execFile(resolveCurlBinary(), args, { maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
| 258 |
if (err) {
|
| 259 |
reject(new CodexApiError(0, `curl failed: ${err.message} ${stderr}`));
|
| 260 |
} else {
|
src/routes/chat.ts
CHANGED
|
@@ -112,7 +112,14 @@ export function createChatRoutes(
|
|
| 112 |
|
| 113 |
const { entryId, token, accountId } = acquired;
|
| 114 |
const codexApi = new CodexApi(token, accountId, cookieJar, entryId);
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
console.log(
|
| 117 |
`[Chat] Account ${entryId} | Codex request:`,
|
| 118 |
JSON.stringify(codexRequest).slice(0, 300),
|
|
@@ -129,12 +136,21 @@ export function createChatRoutes(
|
|
| 129 |
c.header("Connection", "keep-alive");
|
| 130 |
|
| 131 |
return stream(c, async (s) => {
|
|
|
|
| 132 |
try {
|
| 133 |
for await (const chunk of streamCodexToOpenAI(
|
| 134 |
codexApi,
|
| 135 |
rawResponse,
|
| 136 |
codexRequest.model,
|
| 137 |
(u) => { usageInfo = u; },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
)) {
|
| 139 |
await s.write(chunk);
|
| 140 |
}
|
|
@@ -148,6 +164,12 @@ export function createChatRoutes(
|
|
| 148 |
rawResponse,
|
| 149 |
codexRequest.model,
|
| 150 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
accountPool.release(entryId, result.usage);
|
| 152 |
return c.json(result.response);
|
| 153 |
}
|
|
|
|
| 112 |
|
| 113 |
const { entryId, token, accountId } = acquired;
|
| 114 |
const codexApi = new CodexApi(token, accountId, cookieJar, entryId);
|
| 115 |
+
|
| 116 |
+
// Find existing session for multi-turn previous_response_id
|
| 117 |
+
const existingSession = sessionManager.findSession(req.messages);
|
| 118 |
+
const previousResponseId = existingSession?.responseId ?? null;
|
| 119 |
+
const codexRequest = translateToCodexRequest(req, previousResponseId);
|
| 120 |
+
if (previousResponseId) {
|
| 121 |
+
console.log(`[Chat] Account ${entryId} | Multi-turn: previous_response_id=${previousResponseId}`);
|
| 122 |
+
}
|
| 123 |
console.log(
|
| 124 |
`[Chat] Account ${entryId} | Codex request:`,
|
| 125 |
JSON.stringify(codexRequest).slice(0, 300),
|
|
|
|
| 136 |
c.header("Connection", "keep-alive");
|
| 137 |
|
| 138 |
return stream(c, async (s) => {
|
| 139 |
+
let sessionTaskId: string | null = null;
|
| 140 |
try {
|
| 141 |
for await (const chunk of streamCodexToOpenAI(
|
| 142 |
codexApi,
|
| 143 |
rawResponse,
|
| 144 |
codexRequest.model,
|
| 145 |
(u) => { usageInfo = u; },
|
| 146 |
+
(respId) => {
|
| 147 |
+
if (!sessionTaskId) {
|
| 148 |
+
// First call: create session
|
| 149 |
+
sessionTaskId = `task-${Date.now()}`;
|
| 150 |
+
sessionManager.storeSession(sessionTaskId, "turn-1", req.messages);
|
| 151 |
+
}
|
| 152 |
+
sessionManager.updateResponseId(sessionTaskId, respId);
|
| 153 |
+
},
|
| 154 |
)) {
|
| 155 |
await s.write(chunk);
|
| 156 |
}
|
|
|
|
| 164 |
rawResponse,
|
| 165 |
codexRequest.model,
|
| 166 |
);
|
| 167 |
+
// Store session with responseId for multi-turn
|
| 168 |
+
if (result.responseId) {
|
| 169 |
+
const taskId = `task-${Date.now()}`;
|
| 170 |
+
sessionManager.storeSession(taskId, "turn-1", req.messages);
|
| 171 |
+
sessionManager.updateResponseId(taskId, result.responseId);
|
| 172 |
+
}
|
| 173 |
accountPool.release(entryId, result.usage);
|
| 174 |
return c.json(result.response);
|
| 175 |
}
|
src/session/manager.ts
CHANGED
|
@@ -5,6 +5,7 @@ interface Session {
|
|
| 5 |
taskId: string;
|
| 6 |
turnId: string;
|
| 7 |
messageHash: string;
|
|
|
|
| 8 |
createdAt: number;
|
| 9 |
}
|
| 10 |
|
|
@@ -63,10 +64,19 @@ export class SessionManager {
|
|
| 63 |
taskId,
|
| 64 |
turnId,
|
| 65 |
messageHash: hash,
|
|
|
|
| 66 |
createdAt: Date.now(),
|
| 67 |
});
|
| 68 |
}
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
/**
|
| 71 |
* Update turn ID for an existing session
|
| 72 |
*/
|
|
|
|
| 5 |
taskId: string;
|
| 6 |
turnId: string;
|
| 7 |
messageHash: string;
|
| 8 |
+
responseId: string | null;
|
| 9 |
createdAt: number;
|
| 10 |
}
|
| 11 |
|
|
|
|
| 64 |
taskId,
|
| 65 |
turnId,
|
| 66 |
messageHash: hash,
|
| 67 |
+
responseId: null,
|
| 68 |
createdAt: Date.now(),
|
| 69 |
});
|
| 70 |
}
|
| 71 |
|
| 72 |
+
/**
|
| 73 |
+
* Update the response ID for an existing session (for multi-turn previous_response_id)
|
| 74 |
+
*/
|
| 75 |
+
updateResponseId(taskId: string, responseId: string): void {
|
| 76 |
+
const session = this.sessions.get(taskId);
|
| 77 |
+
if (session) session.responseId = responseId;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
/**
|
| 81 |
* Update turn ID for an existing session
|
| 82 |
*/
|
src/tls/curl-binary.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Resolves the curl binary and Chrome TLS profile args.
|
| 3 |
+
*
|
| 4 |
+
* When curl-impersonate is available, we call it directly (NOT via the
|
| 5 |
+
* curl_chrome136 wrapper script) and pass the TLS-level parameters ourselves.
|
| 6 |
+
* This avoids duplicate -H headers between the wrapper and our fingerprint manager.
|
| 7 |
+
*
|
| 8 |
+
* The Chrome TLS args are extracted from curl_chrome136 wrapper script.
|
| 9 |
+
* HTTP headers (-H flags) are intentionally excluded — our fingerprint manager
|
| 10 |
+
* in manager.ts handles those to match Codex Desktop exactly.
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
import { existsSync } from "fs";
|
| 14 |
+
import { execFileSync } from "child_process";
|
| 15 |
+
import { resolve } from "path";
|
| 16 |
+
import { getConfig } from "../config.js";
|
| 17 |
+
|
| 18 |
+
const IS_WIN = process.platform === "win32";
|
| 19 |
+
const BINARY_NAME = IS_WIN ? "curl-impersonate.exe" : "curl-impersonate";
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* Chrome 136 TLS profile parameters.
|
| 23 |
+
* Extracted from curl_chrome136 wrapper (lexiforest/curl-impersonate v1.4.4).
|
| 24 |
+
* These control TLS fingerprint, HTTP/2 framing, and protocol negotiation.
|
| 25 |
+
* HTTP-level headers are NOT included — our fingerprint manager handles those.
|
| 26 |
+
*/
|
| 27 |
+
const CHROME_TLS_ARGS: string[] = [
|
| 28 |
+
// ── TLS cipher suites (exact Chrome 136 order) ──
|
| 29 |
+
"--ciphers",
|
| 30 |
+
[
|
| 31 |
+
"TLS_AES_128_GCM_SHA256",
|
| 32 |
+
"TLS_AES_256_GCM_SHA384",
|
| 33 |
+
"TLS_CHACHA20_POLY1305_SHA256",
|
| 34 |
+
"ECDHE-ECDSA-AES128-GCM-SHA256",
|
| 35 |
+
"ECDHE-RSA-AES128-GCM-SHA256",
|
| 36 |
+
"ECDHE-ECDSA-AES256-GCM-SHA384",
|
| 37 |
+
"ECDHE-RSA-AES256-GCM-SHA384",
|
| 38 |
+
"ECDHE-ECDSA-CHACHA20-POLY1305",
|
| 39 |
+
"ECDHE-RSA-CHACHA20-POLY1305",
|
| 40 |
+
"ECDHE-RSA-AES128-SHA",
|
| 41 |
+
"ECDHE-RSA-AES256-SHA",
|
| 42 |
+
"AES128-GCM-SHA256",
|
| 43 |
+
"AES256-GCM-SHA384",
|
| 44 |
+
"AES128-SHA",
|
| 45 |
+
"AES256-SHA",
|
| 46 |
+
].join(":"),
|
| 47 |
+
// ── Elliptic curves (includes post-quantum X25519MLKEM768) ──
|
| 48 |
+
"--curves", "X25519MLKEM768:X25519:P-256:P-384",
|
| 49 |
+
// ── HTTP/2 with Chrome-exact SETTINGS frame ──
|
| 50 |
+
"--http2",
|
| 51 |
+
"--http2-settings", "1:65536;2:0;4:6291456;6:262144",
|
| 52 |
+
"--http2-window-update", "15663105",
|
| 53 |
+
"--http2-stream-weight", "256",
|
| 54 |
+
"--http2-stream-exclusive", "1",
|
| 55 |
+
// ── TLS extensions (Chrome fingerprint) ──
|
| 56 |
+
"--tlsv1.2",
|
| 57 |
+
"--alps",
|
| 58 |
+
"--tls-permute-extensions",
|
| 59 |
+
"--cert-compression", "brotli",
|
| 60 |
+
"--tls-grease",
|
| 61 |
+
"--tls-use-new-alps-codepoint",
|
| 62 |
+
"--tls-signed-cert-timestamps",
|
| 63 |
+
"--ech", "grease",
|
| 64 |
+
// ── Compression & cookies ──
|
| 65 |
+
"--compressed",
|
| 66 |
+
];
|
| 67 |
+
|
| 68 |
+
let _resolved: string | null = null;
|
| 69 |
+
let _isImpersonate = false;
|
| 70 |
+
let _tlsArgs: string[] | null = null;
|
| 71 |
+
|
| 72 |
+
/**
|
| 73 |
+
* Resolve the curl binary path. Result is cached after first call.
|
| 74 |
+
*/
|
| 75 |
+
export function resolveCurlBinary(): string {
|
| 76 |
+
if (_resolved) return _resolved;
|
| 77 |
+
|
| 78 |
+
const config = getConfig();
|
| 79 |
+
const setting = config.tls.curl_binary;
|
| 80 |
+
|
| 81 |
+
if (setting !== "auto") {
|
| 82 |
+
_resolved = setting;
|
| 83 |
+
_isImpersonate = setting.includes("curl-impersonate");
|
| 84 |
+
console.log(`[TLS] Using configured curl binary: ${_resolved}`);
|
| 85 |
+
return _resolved;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Auto-detect: look for curl-impersonate in bin/
|
| 89 |
+
const binPath = resolve(process.cwd(), "bin", BINARY_NAME);
|
| 90 |
+
if (existsSync(binPath)) {
|
| 91 |
+
_resolved = binPath;
|
| 92 |
+
_isImpersonate = true;
|
| 93 |
+
console.log(`[TLS] Using curl-impersonate: ${_resolved}`);
|
| 94 |
+
return _resolved;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// Fallback to system curl
|
| 98 |
+
_resolved = "curl";
|
| 99 |
+
_isImpersonate = false;
|
| 100 |
+
console.warn(
|
| 101 |
+
`[TLS] curl-impersonate not found at ${binPath}. ` +
|
| 102 |
+
`Falling back to system curl. Run "npm run setup" to install curl-impersonate.`,
|
| 103 |
+
);
|
| 104 |
+
return _resolved;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/**
|
| 108 |
+
* Detect if curl-impersonate supports the --impersonate flag.
|
| 109 |
+
* If supported, returns ["--impersonate", profile] which replaces CHROME_TLS_ARGS.
|
| 110 |
+
* Otherwise returns the manual CHROME_TLS_ARGS.
|
| 111 |
+
*/
|
| 112 |
+
function detectImpersonateSupport(binary: string): string[] {
|
| 113 |
+
try {
|
| 114 |
+
const helpOutput = execFileSync(binary, ["--help", "all"], {
|
| 115 |
+
encoding: "utf-8",
|
| 116 |
+
timeout: 5000,
|
| 117 |
+
});
|
| 118 |
+
if (helpOutput.includes("--impersonate")) {
|
| 119 |
+
const profile = getConfig().tls.impersonate_profile ?? "chrome136";
|
| 120 |
+
console.log(`[TLS] Using --impersonate ${profile}`);
|
| 121 |
+
return ["--impersonate", profile];
|
| 122 |
+
}
|
| 123 |
+
} catch {
|
| 124 |
+
// --help failed, fall back to manual args
|
| 125 |
+
}
|
| 126 |
+
return CHROME_TLS_ARGS;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/**
|
| 130 |
+
* Get Chrome TLS profile args to prepend to curl commands.
|
| 131 |
+
* Returns empty array when using system curl (args are curl-impersonate specific).
|
| 132 |
+
* Uses --impersonate flag when available, otherwise falls back to manual CHROME_TLS_ARGS.
|
| 133 |
+
*/
|
| 134 |
+
export function getChromeTlsArgs(): string[] {
|
| 135 |
+
// Ensure binary is resolved first
|
| 136 |
+
resolveCurlBinary();
|
| 137 |
+
if (!_isImpersonate) return [];
|
| 138 |
+
if (!_tlsArgs) {
|
| 139 |
+
_tlsArgs = detectImpersonateSupport(_resolved!);
|
| 140 |
+
}
|
| 141 |
+
return [..._tlsArgs];
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
/**
|
| 145 |
+
* Reset the cached binary path (useful for testing).
|
| 146 |
+
*/
|
| 147 |
+
export function resetCurlBinaryCache(): void {
|
| 148 |
+
_resolved = null;
|
| 149 |
+
_isImpersonate = false;
|
| 150 |
+
_tlsArgs = null;
|
| 151 |
+
}
|
src/tls/curl-fetch.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Simple GET/POST helpers using curl-impersonate.
|
| 3 |
+
*
|
| 4 |
+
* Drop-in replacement for Node.js fetch() that routes through
|
| 5 |
+
* curl-impersonate with Chrome TLS profile to avoid fingerprinting.
|
| 6 |
+
*
|
| 7 |
+
* Used for non-streaming requests (OAuth, appcast, etc.).
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
import { execFile } from "child_process";
|
| 11 |
+
import { resolveCurlBinary, getChromeTlsArgs } from "./curl-binary.js";
|
| 12 |
+
|
| 13 |
+
export interface CurlFetchResponse {
|
| 14 |
+
status: number;
|
| 15 |
+
body: string;
|
| 16 |
+
ok: boolean;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const STATUS_SEPARATOR = "\n__CURL_HTTP_STATUS__";
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* Perform a GET request via curl-impersonate.
|
| 23 |
+
*/
|
| 24 |
+
export function curlFetchGet(url: string): Promise<CurlFetchResponse> {
|
| 25 |
+
const args = [
|
| 26 |
+
...getChromeTlsArgs(),
|
| 27 |
+
"-s", "-S",
|
| 28 |
+
"--compressed",
|
| 29 |
+
"--max-time", "30",
|
| 30 |
+
"-w", STATUS_SEPARATOR + "%{http_code}",
|
| 31 |
+
url,
|
| 32 |
+
];
|
| 33 |
+
|
| 34 |
+
return execCurl(args);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* Perform a POST request via curl-impersonate.
|
| 39 |
+
*/
|
| 40 |
+
export function curlFetchPost(
|
| 41 |
+
url: string,
|
| 42 |
+
contentType: string,
|
| 43 |
+
body: string,
|
| 44 |
+
): Promise<CurlFetchResponse> {
|
| 45 |
+
const args = [
|
| 46 |
+
...getChromeTlsArgs(),
|
| 47 |
+
"-s", "-S",
|
| 48 |
+
"--compressed",
|
| 49 |
+
"--max-time", "30",
|
| 50 |
+
"-X", "POST",
|
| 51 |
+
"-H", `Content-Type: ${contentType}`,
|
| 52 |
+
"-d", body,
|
| 53 |
+
"-w", STATUS_SEPARATOR + "%{http_code}",
|
| 54 |
+
url,
|
| 55 |
+
];
|
| 56 |
+
|
| 57 |
+
return execCurl(args);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function execCurl(args: string[]): Promise<CurlFetchResponse> {
|
| 61 |
+
return new Promise((resolve, reject) => {
|
| 62 |
+
execFile(
|
| 63 |
+
resolveCurlBinary(),
|
| 64 |
+
args,
|
| 65 |
+
{ maxBuffer: 2 * 1024 * 1024 },
|
| 66 |
+
(err, stdout, stderr) => {
|
| 67 |
+
if (err) {
|
| 68 |
+
reject(new Error(`curl failed: ${err.message} ${stderr}`));
|
| 69 |
+
return;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
const sepIdx = stdout.lastIndexOf(STATUS_SEPARATOR);
|
| 73 |
+
if (sepIdx === -1) {
|
| 74 |
+
reject(new Error(`curl: missing status separator in output`));
|
| 75 |
+
return;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
const body = stdout.slice(0, sepIdx);
|
| 79 |
+
const status = parseInt(stdout.slice(sepIdx + STATUS_SEPARATOR.length), 10);
|
| 80 |
+
|
| 81 |
+
resolve({
|
| 82 |
+
status,
|
| 83 |
+
body,
|
| 84 |
+
ok: status >= 200 && status < 300,
|
| 85 |
+
});
|
| 86 |
+
},
|
| 87 |
+
);
|
| 88 |
+
});
|
| 89 |
+
}
|
src/translation/codex-to-openai.ts
CHANGED
|
@@ -37,6 +37,7 @@ export async function* streamCodexToOpenAI(
|
|
| 37 |
rawResponse: Response,
|
| 38 |
model: string,
|
| 39 |
onUsage?: (usage: UsageInfo) => void,
|
|
|
|
| 40 |
): AsyncGenerator<string> {
|
| 41 |
const chunkId = `chatcmpl-${randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
| 42 |
const created = Math.floor(Date.now() / 1000);
|
|
@@ -63,9 +64,12 @@ export async function* streamCodexToOpenAI(
|
|
| 63 |
switch (evt.event) {
|
| 64 |
case "response.created":
|
| 65 |
case "response.in_progress": {
|
| 66 |
-
// Extract response ID for headers
|
| 67 |
const resp = data.response as Record<string, unknown> | undefined;
|
| 68 |
-
if (resp?.id)
|
|
|
|
|
|
|
|
|
|
| 69 |
break;
|
| 70 |
}
|
| 71 |
|
|
@@ -135,17 +139,25 @@ export async function collectCodexResponse(
|
|
| 135 |
codexApi: CodexApi,
|
| 136 |
rawResponse: Response,
|
| 137 |
model: string,
|
| 138 |
-
): Promise<{ response: ChatCompletionResponse; usage: UsageInfo }> {
|
| 139 |
const id = `chatcmpl-${randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
| 140 |
const created = Math.floor(Date.now() / 1000);
|
| 141 |
let fullText = "";
|
| 142 |
let promptTokens = 0;
|
| 143 |
let completionTokens = 0;
|
|
|
|
| 144 |
|
| 145 |
for await (const evt of codexApi.parseStream(rawResponse)) {
|
| 146 |
const data = evt.data as Record<string, unknown>;
|
| 147 |
|
| 148 |
switch (evt.event) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
case "response.output_text.delta": {
|
| 150 |
const delta = (data.delta as string) ?? "";
|
| 151 |
fullText += delta;
|
|
@@ -153,8 +165,8 @@ export async function collectCodexResponse(
|
|
| 153 |
}
|
| 154 |
|
| 155 |
case "response.completed": {
|
| 156 |
-
// Try to extract usage from the completed response
|
| 157 |
const resp = data.response as Record<string, unknown> | undefined;
|
|
|
|
| 158 |
if (resp?.usage) {
|
| 159 |
const usage = resp.usage as Record<string, number>;
|
| 160 |
promptTokens = usage.input_tokens ?? 0;
|
|
@@ -191,5 +203,6 @@ export async function collectCodexResponse(
|
|
| 191 |
input_tokens: promptTokens,
|
| 192 |
output_tokens: completionTokens,
|
| 193 |
},
|
|
|
|
| 194 |
};
|
| 195 |
}
|
|
|
|
| 37 |
rawResponse: Response,
|
| 38 |
model: string,
|
| 39 |
onUsage?: (usage: UsageInfo) => void,
|
| 40 |
+
onResponseId?: (id: string) => void,
|
| 41 |
): AsyncGenerator<string> {
|
| 42 |
const chunkId = `chatcmpl-${randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
| 43 |
const created = Math.floor(Date.now() / 1000);
|
|
|
|
| 64 |
switch (evt.event) {
|
| 65 |
case "response.created":
|
| 66 |
case "response.in_progress": {
|
| 67 |
+
// Extract response ID for headers and multi-turn
|
| 68 |
const resp = data.response as Record<string, unknown> | undefined;
|
| 69 |
+
if (resp?.id) {
|
| 70 |
+
responseId = resp.id as string;
|
| 71 |
+
onResponseId?.(responseId);
|
| 72 |
+
}
|
| 73 |
break;
|
| 74 |
}
|
| 75 |
|
|
|
|
| 139 |
codexApi: CodexApi,
|
| 140 |
rawResponse: Response,
|
| 141 |
model: string,
|
| 142 |
+
): Promise<{ response: ChatCompletionResponse; usage: UsageInfo; responseId: string | null }> {
|
| 143 |
const id = `chatcmpl-${randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
| 144 |
const created = Math.floor(Date.now() / 1000);
|
| 145 |
let fullText = "";
|
| 146 |
let promptTokens = 0;
|
| 147 |
let completionTokens = 0;
|
| 148 |
+
let responseId: string | null = null;
|
| 149 |
|
| 150 |
for await (const evt of codexApi.parseStream(rawResponse)) {
|
| 151 |
const data = evt.data as Record<string, unknown>;
|
| 152 |
|
| 153 |
switch (evt.event) {
|
| 154 |
+
case "response.created":
|
| 155 |
+
case "response.in_progress": {
|
| 156 |
+
const resp = data.response as Record<string, unknown> | undefined;
|
| 157 |
+
if (resp?.id) responseId = resp.id as string;
|
| 158 |
+
break;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
case "response.output_text.delta": {
|
| 162 |
const delta = (data.delta as string) ?? "";
|
| 163 |
fullText += delta;
|
|
|
|
| 165 |
}
|
| 166 |
|
| 167 |
case "response.completed": {
|
|
|
|
| 168 |
const resp = data.response as Record<string, unknown> | undefined;
|
| 169 |
+
if (resp?.id) responseId = resp.id as string;
|
| 170 |
if (resp?.usage) {
|
| 171 |
const usage = resp.usage as Record<string, number>;
|
| 172 |
promptTokens = usage.input_tokens ?? 0;
|
|
|
|
| 203 |
input_tokens: promptTokens,
|
| 204 |
output_tokens: completionTokens,
|
| 205 |
},
|
| 206 |
+
responseId,
|
| 207 |
};
|
| 208 |
}
|
src/translation/openai-to-codex.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
| 2 |
* Translate OpenAI Chat Completions request → Codex Responses API request.
|
| 3 |
*/
|
| 4 |
|
|
|
|
|
|
|
| 5 |
import type { ChatCompletionRequest } from "../types/openai.js";
|
| 6 |
import type {
|
| 7 |
CodexResponsesRequest,
|
|
@@ -10,6 +12,19 @@ import type {
|
|
| 10 |
import { resolveModelId, getModelInfo } from "../routes/models.js";
|
| 11 |
import { getConfig } from "../config.js";
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
/**
|
| 14 |
* Convert a ChatCompletionRequest to a CodexResponsesRequest.
|
| 15 |
*
|
|
@@ -21,12 +36,16 @@ import { getConfig } from "../config.js";
|
|
| 21 |
*/
|
| 22 |
export function translateToCodexRequest(
|
| 23 |
req: ChatCompletionRequest,
|
|
|
|
| 24 |
): CodexResponsesRequest {
|
| 25 |
// Collect system messages as instructions
|
| 26 |
const systemMessages = req.messages.filter((m) => m.role === "system");
|
| 27 |
-
const
|
| 28 |
systemMessages.map((m) => m.content).join("\n\n") ||
|
| 29 |
"You are a helpful assistant.";
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
// Build input items from non-system messages
|
| 32 |
const input: CodexInputItem[] = [];
|
|
@@ -58,6 +77,11 @@ export function translateToCodexRequest(
|
|
| 58 |
tools: [],
|
| 59 |
};
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
// Add reasoning effort if applicable
|
| 62 |
const effort =
|
| 63 |
req.reasoning_effort ??
|
|
|
|
| 2 |
* Translate OpenAI Chat Completions request → Codex Responses API request.
|
| 3 |
*/
|
| 4 |
|
| 5 |
+
import { readFileSync } from "fs";
|
| 6 |
+
import { resolve } from "path";
|
| 7 |
import type { ChatCompletionRequest } from "../types/openai.js";
|
| 8 |
import type {
|
| 9 |
CodexResponsesRequest,
|
|
|
|
| 12 |
import { resolveModelId, getModelInfo } from "../routes/models.js";
|
| 13 |
import { getConfig } from "../config.js";
|
| 14 |
|
| 15 |
+
const DESKTOP_CONTEXT = loadDesktopContext();
|
| 16 |
+
|
| 17 |
+
function loadDesktopContext(): string {
|
| 18 |
+
try {
|
| 19 |
+
return readFileSync(
|
| 20 |
+
resolve(process.cwd(), "config/prompts/desktop-context.md"),
|
| 21 |
+
"utf-8",
|
| 22 |
+
);
|
| 23 |
+
} catch {
|
| 24 |
+
return "";
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
/**
|
| 29 |
* Convert a ChatCompletionRequest to a CodexResponsesRequest.
|
| 30 |
*
|
|
|
|
| 36 |
*/
|
| 37 |
export function translateToCodexRequest(
|
| 38 |
req: ChatCompletionRequest,
|
| 39 |
+
previousResponseId?: string | null,
|
| 40 |
): CodexResponsesRequest {
|
| 41 |
// Collect system messages as instructions
|
| 42 |
const systemMessages = req.messages.filter((m) => m.role === "system");
|
| 43 |
+
const userInstructions =
|
| 44 |
systemMessages.map((m) => m.content).join("\n\n") ||
|
| 45 |
"You are a helpful assistant.";
|
| 46 |
+
const instructions = DESKTOP_CONTEXT
|
| 47 |
+
? `${DESKTOP_CONTEXT}\n\n${userInstructions}`
|
| 48 |
+
: userInstructions;
|
| 49 |
|
| 50 |
// Build input items from non-system messages
|
| 51 |
const input: CodexInputItem[] = [];
|
|
|
|
| 77 |
tools: [],
|
| 78 |
};
|
| 79 |
|
| 80 |
+
// Add previous response ID for multi-turn conversations
|
| 81 |
+
if (previousResponseId) {
|
| 82 |
+
request.previous_response_id = previousResponseId;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
// Add reasoning effort if applicable
|
| 86 |
const effort =
|
| 87 |
req.reasoning_effort ??
|
src/update-checker.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { resolve } from "path";
|
|
| 8 |
import yaml from "js-yaml";
|
| 9 |
import { mutateClientConfig } from "./config.js";
|
| 10 |
import { jitterInt } from "./utils/jitter.js";
|
|
|
|
| 11 |
|
| 12 |
const CONFIG_PATH = resolve(process.cwd(), "config/default.yaml");
|
| 13 |
const STATE_PATH = resolve(process.cwd(), "data/update-state.json");
|
|
@@ -44,8 +45,13 @@ function parseAppcast(xml: string): {
|
|
| 44 |
const itemMatch = xml.match(/<item>([\s\S]*?)<\/item>/i);
|
| 45 |
if (!itemMatch) return { version: null, build: null, downloadUrl: null };
|
| 46 |
const item = itemMatch[1];
|
| 47 |
-
|
| 48 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
const urlMatch = item.match(/url="([^"]+)"/);
|
| 50 |
return {
|
| 51 |
version: versionMatch?.[1] ?? null,
|
|
@@ -64,11 +70,11 @@ function applyVersionUpdate(version: string, build: string): void {
|
|
| 64 |
|
| 65 |
export async function checkForUpdate(): Promise<UpdateState> {
|
| 66 |
const current = loadCurrentConfig();
|
| 67 |
-
const res = await
|
| 68 |
if (!res.ok) {
|
| 69 |
-
throw new Error(`Failed to fetch appcast: ${res.status}
|
| 70 |
}
|
| 71 |
-
const xml =
|
| 72 |
const { version, build, downloadUrl } = parseAppcast(xml);
|
| 73 |
|
| 74 |
const updateAvailable = !!(version && build &&
|
|
|
|
| 8 |
import yaml from "js-yaml";
|
| 9 |
import { mutateClientConfig } from "./config.js";
|
| 10 |
import { jitterInt } from "./utils/jitter.js";
|
| 11 |
+
import { curlFetchGet } from "./tls/curl-fetch.js";
|
| 12 |
|
| 13 |
const CONFIG_PATH = resolve(process.cwd(), "config/default.yaml");
|
| 14 |
const STATE_PATH = resolve(process.cwd(), "data/update-state.json");
|
|
|
|
| 45 |
const itemMatch = xml.match(/<item>([\s\S]*?)<\/item>/i);
|
| 46 |
if (!itemMatch) return { version: null, build: null, downloadUrl: null };
|
| 47 |
const item = itemMatch[1];
|
| 48 |
+
// Support both attribute syntax (sparkle:version="X") and element syntax (<sparkle:version>X</sparkle:version>)
|
| 49 |
+
const versionMatch =
|
| 50 |
+
item.match(/sparkle:shortVersionString="([^"]+)"/) ??
|
| 51 |
+
item.match(/<sparkle:shortVersionString>([^<]+)<\/sparkle:shortVersionString>/);
|
| 52 |
+
const buildMatch =
|
| 53 |
+
item.match(/sparkle:version="([^"]+)"/) ??
|
| 54 |
+
item.match(/<sparkle:version>([^<]+)<\/sparkle:version>/);
|
| 55 |
const urlMatch = item.match(/url="([^"]+)"/);
|
| 56 |
return {
|
| 57 |
version: versionMatch?.[1] ?? null,
|
|
|
|
| 70 |
|
| 71 |
export async function checkForUpdate(): Promise<UpdateState> {
|
| 72 |
const current = loadCurrentConfig();
|
| 73 |
+
const res = await curlFetchGet(APPCAST_URL);
|
| 74 |
if (!res.ok) {
|
| 75 |
+
throw new Error(`Failed to fetch appcast: ${res.status}`);
|
| 76 |
}
|
| 77 |
+
const xml = res.body;
|
| 78 |
const { version, build, downloadUrl } = parseAppcast(xml);
|
| 79 |
|
| 80 |
const updateAvailable = !!(version && build &&
|