Skip to content

Commit bf9517f

Browse files
committed
fix: route logs to TUI app panel to prevent message overlap
- Refactor logger to write to client.app.log when available - Gate console output behind CODEX_CONSOLE_LOG=1 env var - Replace all direct console.log/warn/error calls with logger functions - Add initLogger(client) for TUI integration at plugin startup - Fix security vulnerability: update hono 4.11.5 → 4.11.7 - Remove unused PLUGIN_NAME import in context-overflow.ts - Update test mocks to use logger instead of console.warn
1 parent edb9a58 commit bf9517f

9 files changed

Lines changed: 262 additions & 319 deletions

File tree

AGENTS.md

Lines changed: 47 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -1,210 +1,57 @@
1-
# AGENTS.md
1+
# PROJECT KNOWLEDGE BASE
22

3-
This file provides coding guidance for AI agents (including Claude Code, Codex, and others) when working with code in this repository.
3+
Generated: 2026-01-29
4+
Commit: 693b0cc
5+
Branch: main
46

5-
## Overview
7+
## OVERVIEW
8+
OpenCode plugin that swaps OpenAI SDK calls to the ChatGPT Codex backend with multi-account OAuth.
69

7-
This is an **opencode plugin** that enables OAuth authentication with OpenAI's ChatGPT Plus/Pro Codex backend. It allows users to access `gpt-5.2-codex`, `gpt-5.1-codex`, `gpt-5.1-codex-max`, `gpt-5.1-codex-mini`, `gpt-5.2`, and `gpt-5.1` models through their ChatGPT subscription instead of using OpenAI Platform API credits. Legacy GPT-5.0 models are automatically normalized to their GPT-5.1 equivalents.
8-
9-
**Key architecture principle**: 7-step fetch flow that intercepts opencode's OpenAI SDK requests, transforms them for the ChatGPT backend API, and handles OAuth token management.
10-
11-
## Build & Test Commands
10+
## STRUCTURE
11+
```
12+
./
13+
├── index.ts # plugin entry point (not under src/)
14+
├── lib/ # main source (auth, request, prompts, config)
15+
├── test/ # vitest suites
16+
├── scripts/ # install + build helpers
17+
├── assets/ # static assets
18+
├── config/ # OpenCode config examples
19+
├── docs/ # architecture docs/diagrams
20+
├── dist/ # build output (generated)
21+
└── SECURITY.md # vuln reporting rules
22+
```
1223

24+
## WHERE TO LOOK
25+
| Task | Location | Notes |
26+
| --- | --- | --- |
27+
| Fetch flow orchestration | `index.ts` | 7-step request pipeline
28+
| OAuth flow + tokens | `lib/auth/auth.ts` | PKCE, refresh, JWT decode
29+
| OAuth callback server | `lib/auth/server.ts` | binds port 1455
30+
| Request mutation | `lib/request/request-transformer.ts` | model normalization + prompts
31+
| Request helpers | `lib/request/fetch-helpers.ts` | headers, rate limit handling
32+
| SSE response handling | `lib/request/response-handler.ts` | SSE to JSON
33+
| Prompt fetching/cache | `lib/prompts/codex.ts` | GitHub release ETag cache
34+
| Config parsing | `lib/config.ts` | CODEX_MODE + options
35+
| Tests | `test/` | vitest globals enabled
36+
37+
## CONVENTIONS
38+
- Source lives in `index.ts` and `lib/`; `dist/` is generated.
39+
- ESLint flat config: no `any`, unused args must be prefixed with `_`.
40+
- Test files relax lint rules; see `eslint.config.js`.
41+
- Build must copy `lib/oauth-success.html` into `dist/lib/` (see `scripts/copy-oauth-success.js`).
42+
43+
## ANTI-PATTERNS (THIS PROJECT)
44+
- Do not edit `dist/` outputs or `tmp*` directories.
45+
- Do not open public security issues; follow `SECURITY.md` for reporting.
46+
47+
## COMMANDS
1348
```bash
14-
# Build (compiles TypeScript + copies HTML file)
1549
npm run build
16-
17-
# Type checking only (no build)
1850
npm run typecheck
19-
20-
# Run all tests
2151
npm test
22-
23-
# Watch mode for TDD
24-
npm run test:watch
25-
26-
# Interactive test UI
27-
npm run test:ui
28-
29-
# Coverage report
30-
npm run test:coverage
52+
npm run lint
3153
```
3254

33-
**Important**: The build script has a critical step that copies `lib/oauth-success.html` to `dist/lib/`. This HTML file is required for the OAuth callback flow.
34-
35-
## Code Architecture
36-
37-
### Plugin Flow (index.ts)
38-
39-
The main entry point orchestrates a **7-step fetch flow**:
40-
41-
1. **Token Management**: Check token expiration, refresh if needed
42-
2. **URL Rewriting**: Transform OpenAI Platform API URLs → ChatGPT backend API (`https://chatgpt.com/backend-api/codex/responses`)
43-
3. **Request Transformation**:
44-
- Normalize model names (all variants → `gpt-5.2`, `gpt-5.2-codex`, `gpt-5.1`, `gpt-5.1-codex`, `gpt-5.1-codex-max`, `gpt-5.1-codex-mini`, `gpt-5`, `gpt-5-codex`, or `codex-mini-latest`)
45-
- Inject Codex system instructions from latest GitHub release
46-
- Apply reasoning configuration (effort, summary, verbosity)
47-
- Add CODEX_MODE bridge prompt (default) or tool remap message (legacy)
48-
- Filter OpenCode system prompts when in CODEX_MODE
49-
- Filter conversation history (remove `rs_*` IDs for stateless operation)
50-
4. **Headers**: Add OAuth token + ChatGPT account ID
51-
5. **Request Execution**: Send to Codex backend
52-
6. **Response Logging**: Optional debug logging (ENABLE_PLUGIN_REQUEST_LOGGING=1)
53-
7. **Response Handling**: Convert SSE to JSON (non-tool requests) or pass through
54-
55-
### Module Organization
56-
57-
**Core Plugin** (`index.ts`)
58-
- Plugin definition and main fetch orchestration
59-
- OAuth loader (extracts ChatGPT account ID from JWT)
60-
- Configuration loading and CODEX_MODE determination
61-
62-
**Authentication** (`lib/auth/`)
63-
- `auth.ts`: OAuth flow (PKCE, token exchange, JWT decoding, refresh)
64-
- `server.ts`: Local HTTP server for OAuth callback (port 1455)
65-
- `browser.ts`: Platform-specific browser opening
66-
67-
**Request Handling** (`lib/request/`)
68-
- `fetch-helpers.ts`: 10 focused helper functions for main fetch flow
69-
- `request-transformer.ts`: Body transformations (model normalization, reasoning config, input filtering)
70-
- `response-handler.ts`: SSE to JSON conversion
71-
72-
**Prompts** (`lib/prompts/`)
73-
- `codex.ts`: Fetches Codex instructions from GitHub (ETag-cached), tool remap message
74-
- `codex-opencode-bridge.ts`: CODEX_MODE bridge prompt for CLI parity
75-
76-
**Configuration** (`lib/`)
77-
- `config.ts`: Plugin config loading, CODEX_MODE determination
78-
- `constants.ts`: All magic values, URLs, error messages
79-
- `types.ts`: TypeScript type definitions
80-
- `logger.ts`: Debug logging (controlled by env var)
81-
82-
### Key Design Patterns
83-
84-
**1. Stateless Operation**: Uses `store: false` + `include: ["reasoning.encrypted_content"]`
85-
- Allows multi-turn conversations without server-side storage
86-
- Encrypted reasoning content persists context across turns
87-
88-
**2. CODEX_MODE** (enabled by default):
89-
- **Priority**: `CODEX_MODE` env var > `~/.opencode/openai-codex-auth-config.json` > default (true)
90-
- When enabled: Filters out OpenCode system prompts, adds Codex-OpenCode bridge prompt with Task tool & MCP awareness
91-
- When disabled: Uses legacy tool remap message
92-
- Bridge prompt (~550 tokens): Tool mappings, available tools, working style, **Task tool/sub-agent awareness**, **MCP tool awareness**
93-
- **Prompt verification**: Caches OpenCode's codex.txt from GitHub (ETag-based) to verify exact prompt removal, with fallback to text signature matching
94-
95-
**3. Configuration Merging**:
96-
- Global options (`provider.openai.options`) + per-model options (`provider.openai.models[name].options`)
97-
- Model-specific options override global
98-
- Plugin defaults: `reasoningEffort: "medium"`, `reasoningSummary: "auto"`, `textVerbosity: "medium"`
99-
100-
**4. Model Normalization** (GPT-5.0 → GPT-5.1 migration):
101-
- All `gpt-5.2-codex*` variants → `gpt-5.2-codex` (newest Codex model, supports xhigh)
102-
- All `gpt-5.1-codex-max*` variants → `gpt-5.1-codex-max`
103-
- All `gpt-5.1-codex*` variants → `gpt-5.1-codex`
104-
- All `gpt-5.1-codex-mini*` variants → `gpt-5.1-codex-mini`
105-
- All `gpt-5.2` variants → `gpt-5.2`
106-
- All `gpt-5.1` variants → `gpt-5.1`
107-
- **Legacy mappings** (GPT-5.0 being phased out):
108-
- `gpt-5-codex*` variants → `gpt-5.1-codex`
109-
- `gpt-5-codex-mini*` or `codex-mini-latest``gpt-5.1-codex-mini`
110-
- `gpt-5*` variants (including `gpt-5-mini`, `gpt-5-nano`) → `gpt-5.1`
111-
- `minimal` effort auto-normalized to `low` for Codex families (including GPT-5.2 Codex) and clamped to `medium` (or `high` when requested) for Codex Mini
112-
113-
**5. Model-Specific Prompt Selection**:
114-
- Different prompts for different model families (matching Codex CLI):
115-
- `gpt-5.2-codex*``gpt-5.2-codex_prompt.md` (117 lines, Codex CLI agent prompt)
116-
- `gpt-5.1-codex-max*``gpt-5.1-codex-max_prompt.md` (117 lines, frontend design guidelines)
117-
- `gpt-5.1-codex*`, `codex-*``gpt_5_codex_prompt.md` (105 lines, coding focus)
118-
- `gpt-5.2*``gpt_5_2_prompt.md` (GPT‑5.2 general family)
119-
- `gpt-5.1*``gpt_5_1_prompt.md` (368 lines, full behavioral guidance)
120-
- `getModelFamily()` determines prompt selection based on normalized model
121-
122-
**6. Codex Instructions Caching**:
123-
- Fetches from latest release tag (not main branch)
124-
- ETag-based HTTP conditional requests per model family
125-
- Separate cache files per family: `gpt-5.2-codex-instructions.md`, `codex-max-instructions.md`, `codex-instructions.md`, `gpt-5.2-instructions.md`, `gpt-5.1-instructions.md`
126-
- Cache invalidation when release tag changes
127-
- Falls back to bundled version if GitHub unavailable
128-
129-
## Development Patterns
130-
131-
### Adding New Configuration Options
132-
133-
1. Add to `ConfigOptions` interface in `lib/types.ts`
134-
2. Update `transformRequestBody()` in `lib/request/request-transformer.ts`
135-
3. Add tests in `test/request-transformer.test.ts`
136-
4. Document in README.md configuration section
137-
138-
### Modifying Request Transformation
139-
140-
All request transformations go through `transformRequestBody()`:
141-
- Input filtering: `filterInput()`, `filterOpenCodeSystemPrompts()`
142-
- Message injection: `addCodexBridgeMessage()` or `addToolRemapMessage()`
143-
- Reasoning config: `getReasoningConfig()` (follows Codex CLI defaults, not opencode defaults)
144-
- Model config: `getModelConfig()` (merges global + per-model options)
145-
146-
### OAuth Flow Modifications
147-
148-
OAuth implementation follows OpenAI Codex CLI patterns:
149-
- Client ID: `app_EMoamEEZ73f0CkXaXp7hrann`
150-
- PKCE with S256 challenge
151-
- Special params: `codex_cli_simplified_flow=true`, `originator=codex_cli_rs`
152-
- Callback server on port 1455 (matches Codex CLI)
153-
154-
### Testing Strategy
155-
156-
- **191 comprehensive tests** covering all modules
157-
- Test files mirror source structure (`test/auth.test.ts``lib/auth/auth.ts`)
158-
- Mock-heavy testing (no actual network calls or file I/O in tests)
159-
- Focus on edge cases: token expiration, model normalization, input filtering, CODEX_MODE toggling
160-
161-
## Important Configuration Differences
162-
163-
This plugin **intentionally differs from opencode defaults** because it accesses ChatGPT backend API (not OpenAI Platform API):
164-
165-
| Setting | opencode Default | This Plugin Default | Reason |
166-
|---------|-----------------|---------------------|--------|
167-
| `reasoningEffort` | "high" (gpt-5) | "medium" (Codex Max defaults to "high") | Matches Codex CLI default and Codex Max capabilities |
168-
| `textVerbosity` | "low" (gpt-5) | "medium" | Matches Codex CLI default |
169-
| `reasoningSummary` | "detailed" | "auto" | Matches Codex CLI default |
170-
| gpt-5-codex config | (excluded) | Full support | opencode excludes gpt-5-codex from auto-config |
171-
| `store` | true | false | Required for ChatGPT backend |
172-
| `include` | (not set) | `["reasoning.encrypted_content"]` | Required for stateless operation |
173-
174-
## File Paths & Locations
175-
176-
- **Plugin config**: `~/.opencode/openai-codex-auth-config.json`
177-
- **Cache dir**: `~/.opencode/cache/`
178-
- `codex-instructions.md` (Codex CLI instructions from GitHub)
179-
- `codex-instructions-meta.json` (ETag + release tag for Codex instructions)
180-
- `opencode-codex.txt` (OpenCode system prompt from GitHub, for verification)
181-
- `opencode-codex-meta.json` (ETag for OpenCode prompt)
182-
- **Debug logs**: `~/.opencode/logs/codex-plugin/` (when `ENABLE_PLUGIN_REQUEST_LOGGING=1`)
183-
- **OAuth callback**: `http://localhost:1455/auth/callback`
184-
185-
## Environment Variables
186-
187-
- `CODEX_MODE`: Override config file (1=enable, 0=disable)
188-
- `ENABLE_PLUGIN_REQUEST_LOGGING`: Enable detailed request logging (1=enable)
189-
190-
## TypeScript Configuration
191-
192-
- Target: ES2022
193-
- Module: ES2022 with bundler resolution
194-
- Output: `./dist/`
195-
- Strict mode enabled
196-
- Declaration files generated
197-
- Source maps enabled
198-
- Excludes: `test/`, `node_modules/`, `dist/`
199-
200-
## Dependencies
201-
202-
**Production**:
203-
- `@openauthjs/openauth` (OAuth PKCE implementation)
204-
205-
**Development**:
206-
- `@opencode-ai/plugin` (peer dependency)
207-
- `vitest` (testing framework)
208-
- TypeScript
209-
210-
**Zero external runtime dependencies** - only uses Node.js built-ins for file I/O, HTTP, crypto.
55+
## NOTES
56+
- OAuth callback server binds `http://127.0.0.1:1455/auth/callback`.
57+
- ChatGPT backend requires stateless requests (`store: false`, include encrypted reasoning).

lib/auth/auth.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { generatePKCE } from "@openauthjs/openauth/pkce";
22
import { randomBytes } from "node:crypto";
33
import type { PKCEPair, AuthorizationFlow, TokenResult, ParsedAuthInput, JWTPayload } from "../types.js";
4+
import { logError } from "../logger.js";
45

56
// OAuth constants (from openai/codex)
67
export const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
@@ -85,7 +86,7 @@ export async function exchangeAuthorizationCode(
8586
});
8687
if (!res.ok) {
8788
const text = await res.text().catch(() => "");
88-
console.error("[openai-codex-plugin] code->token failed:", res.status, text);
89+
logError(`code->token failed: ${res.status} ${text}`);
8990
return { type: "failed", reason: "http_error", statusCode: res.status, message: text || undefined };
9091
}
9192
const json = (await res.json()) as {
@@ -99,7 +100,7 @@ export async function exchangeAuthorizationCode(
99100
!json?.refresh_token ||
100101
typeof json?.expires_in !== "number"
101102
) {
102-
console.error("[openai-codex-plugin] token response missing fields:", json);
103+
logError("token response missing fields", json);
103104
return { type: "failed", reason: "invalid_response", message: "Missing access_token, refresh_token, or expires_in" };
104105
}
105106
return {
@@ -153,11 +154,7 @@ export async function refreshAccessToken(refreshToken: string): Promise<TokenRes
153154

154155
if (!response.ok) {
155156
const text = await response.text().catch(() => "");
156-
console.error(
157-
"[openai-codex-plugin] Token refresh failed:",
158-
response.status,
159-
text,
160-
);
157+
logError(`Token refresh failed: ${response.status} ${text}`);
161158
return { type: "failed", reason: "http_error", statusCode: response.status, message: text || undefined };
162159
}
163160

@@ -168,16 +165,13 @@ export async function refreshAccessToken(refreshToken: string): Promise<TokenRes
168165
id_token?: string;
169166
};
170167
if (!json?.access_token || typeof json?.expires_in !== "number") {
171-
console.error(
172-
"[openai-codex-plugin] Token refresh response missing fields:",
173-
json,
174-
);
168+
logError("Token refresh response missing fields", json);
175169
return { type: "failed", reason: "invalid_response", message: "Missing access_token or expires_in" };
176170
}
177171

178172
const nextRefresh = json.refresh_token ?? refreshToken;
179173
if (!nextRefresh) {
180-
console.error("[openai-codex-plugin] Token refresh missing refresh token");
174+
logError("Token refresh missing refresh token");
181175
return { type: "failed", reason: "missing_refresh", message: "No refresh token in response or input" };
182176
}
183177

@@ -191,7 +185,7 @@ export async function refreshAccessToken(refreshToken: string): Promise<TokenRes
191185
};
192186
} catch (error) {
193187
const err = error as Error;
194-
console.error("[openai-codex-plugin] Token refresh error:", err);
188+
logError("Token refresh error", err);
195189
return { type: "failed", reason: "network_error", message: err?.message };
196190
}
197191
}

lib/auth/server.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from "node:fs";
33
import path from "node:path";
44
import { fileURLToPath } from "node:url";
55
import type { OAuthServerInfo } from "../types.js";
6+
import { logError, logWarn } from "../logger.js";
67

78
// Resolve path to oauth-success.html (one level up from auth/ subfolder)
89
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -38,7 +39,7 @@ export function startLocalOAuthServer({ state }: { state: string }): Promise<OAu
3839
res.end(successHtml);
3940
(server as http.Server & { _lastCode?: string })._lastCode = code;
4041
} catch (err) {
41-
console.error("[openai-codex-plugin] Request handler error:", (err as Error)?.message ?? String(err));
42+
logError(`Request handler error: ${(err as Error)?.message ?? String(err)}`);
4243
res.statusCode = 500;
4344
res.end("Internal error");
4445
}
@@ -61,16 +62,14 @@ export function startLocalOAuthServer({ state }: { state: string }): Promise<OAu
6162
if (lastCode) return { code: lastCode };
6263
await poll();
6364
}
64-
console.error("[openai-codex-plugin] OAuth poll timeout after 5 minutes");
65+
logWarn("OAuth poll timeout after 5 minutes");
6566
return null;
6667
},
6768
});
6869
})
6970
.on("error", (err: NodeJS.ErrnoException) => {
70-
console.error(
71-
"[openai-codex-plugin] Failed to bind http://127.0.0.1:1455 (",
72-
err?.code,
73-
") Falling back to manual paste.",
71+
logError(
72+
`Failed to bind http://127.0.0.1:1455 (${err?.code}). Falling back to manual paste.`,
7473
);
7574
resolve({
7675
port: 1455,
@@ -79,7 +78,7 @@ export function startLocalOAuthServer({ state }: { state: string }): Promise<OAu
7978
try {
8079
server.close();
8180
} catch (err) {
82-
console.error("[openai-codex-plugin] Failed to close OAuth server:", (err as Error)?.message ?? String(err));
81+
logError(`Failed to close OAuth server: ${(err as Error)?.message ?? String(err)}`);
8382
}
8483
},
8584
waitForCode: async () => null,

lib/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readFileSync, existsSync } from "node:fs";
22
import { join } from "node:path";
33
import { homedir } from "node:os";
44
import type { PluginConfig } from "./types.js";
5+
import { logWarn } from "./logger.js";
56

67
const CONFIG_PATH = join(homedir(), ".opencode", "openai-codex-auth-config.json");
78

@@ -41,9 +42,8 @@ export function loadPluginConfig(): PluginConfig {
4142
...userConfig,
4243
};
4344
} catch (error) {
44-
console.warn(
45-
`[openai-codex-plugin] Failed to load config from ${CONFIG_PATH}:`,
46-
(error as Error).message
45+
logWarn(
46+
`Failed to load config from ${CONFIG_PATH}: ${(error as Error).message}`,
4747
);
4848
return DEFAULT_CONFIG;
4949
}

0 commit comments

Comments
 (0)