Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ Follow me on [X @nummanthinks](https://x.com/nummanthinks) for future updates an
- βœ… **Auto-refreshing tokens** - Handles token expiration automatically
- βœ… **Smart auto-updating Codex instructions** - Tracks latest stable release with ETag caching
- βœ… **Full tool support** - write, edit, bash, grep, glob, and more
- βœ… **CODEX_MODE** - Codex-OpenCode bridge prompt for CLI parity (enabled by default)
- βœ… **Automatic tool remapping** - Codex tools β†’ opencode tools
- βœ… **Configurable reasoning** - Control effort, summary verbosity, and text output
- βœ… **Type-safe & tested** - Strict TypeScript with 93 comprehensive tests
- βœ… **Type-safe & tested** - Strict TypeScript with 123 comprehensive tests
- βœ… **Modular architecture** - Easy to maintain and extend

## Installation
Expand Down Expand Up @@ -334,6 +335,32 @@ This repository includes ready-to-use configuration examples:

Copy the appropriate file to your opencode configuration location and customize as needed.

### Plugin Configuration

The plugin supports configuration via `~/.opencode/openai-codex-auth-config.json`:

```json
{
"codexMode": true
}
```

#### CODEX_MODE Setting

- **`codexMode: true`** (default) - Uses Codex-OpenCode bridge prompt for better Codex CLI parity
- **`codexMode: false`** - Uses tool remap message (legacy mode)

**Priority order**: `CODEX_MODE` environment variable > config file > default (true)

**Examples**:
```bash
# Override config file with environment variable
CODEX_MODE=0 opencode run "task" # Disable CODEX_MODE temporarily
CODEX_MODE=1 opencode run "task" # Enable CODEX_MODE temporarily
```

> **Note**: CODEX_MODE is enabled by default for optimal Codex CLI compatibility. Only disable if you experience issues with the bridge prompt.

## How It Works

The plugin implements a 7-step fetch flow in TypeScript:
Expand All @@ -344,7 +371,8 @@ The plugin implements a 7-step fetch flow in TypeScript:
- Model normalization (`gpt-5-codex` variants β†’ `gpt-5-codex`, `gpt-5` variants β†’ `gpt-5`)
- Injects Codex instructions from latest [openai/codex](https://github.com/openai/codex) release
- Applies reasoning configuration (effort, summary, verbosity)
- Adds tool remapping instructions (`apply_patch` β†’ `edit`, `update_plan` β†’ `todowrite`)
- Adds Codex-OpenCode bridge prompt (CODEX_MODE=true, default) or tool remap message (CODEX_MODE=false)
- Filters OpenCode system prompts when in CODEX_MODE
- Filters conversation history (removes stored IDs for stateless operation)
4. **Headers**: Adds OAuth token and ChatGPT account ID headers
5. **Request Execution**: Sends to Codex backend API
Expand All @@ -355,9 +383,10 @@ The plugin implements a 7-step fetch flow in TypeScript:

- **Modular Design**: 10 focused helper functions, each < 40 lines
- **Type-Safe**: Strict TypeScript with comprehensive type definitions
- **Tested**: 93 tests covering all functionality
- **Tested**: 123 tests covering all functionality including CODEX_MODE
- **Zero Dependencies**: Only uses @openauthjs/openauth
- **Codex Instructions**: ETag-cached from GitHub, auto-updates on new releases
- **CODEX_MODE**: Configurable bridge prompt for Codex CLI parity (enabled by default)
- **Stateless Operation**: Uses `store: false` with encrypted reasoning content for multi-turn conversations

## Limitations
Expand Down
18 changes: 12 additions & 6 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Plugin, PluginInput } from "@opencode-ai/plugin";
import type { Auth } from "@opencode-ai/sdk";
import { createAuthorizationFlow, exchangeAuthorizationCode, decodeJWT, REDIRECT_URI } from "./lib/auth.js";
import { getCodexInstructions } from "./lib/codex.js";
import { startLocalOAuthServer } from "./lib/server.js";
import { createAuthorizationFlow, exchangeAuthorizationCode, decodeJWT, REDIRECT_URI } from "./lib/auth/auth.js";
import { getCodexInstructions } from "./lib/prompts/codex.js";
import { startLocalOAuthServer } from "./lib/auth/server.js";
import { logRequest } from "./lib/logger.js";
import { openBrowserUrl } from "./lib/browser.js";
import { openBrowserUrl } from "./lib/auth/browser.js";
import {
shouldRefreshToken,
refreshAndUpdateToken,
Expand All @@ -14,7 +14,8 @@ import {
createCodexHeaders,
handleErrorResponse,
handleSuccessResponse,
} from "./lib/fetch-helpers.js";
} from "./lib/request/fetch-helpers.js";
import { loadPluginConfig, getCodexMode } from "./lib/config.js";
import type { UserConfig } from "./lib/types.js";
import {
DUMMY_API_KEY,
Expand Down Expand Up @@ -84,6 +85,11 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
models: providerConfig?.models || {},
};

// Load plugin configuration and determine CODEX_MODE
// Priority: CODEX_MODE env var > config file > default (true)
const pluginConfig = loadPluginConfig();
const codexMode = getCodexMode(pluginConfig);

// Fetch Codex system instructions (cached with ETag for efficiency)
const CODEX_INSTRUCTIONS = await getCodexInstructions();

Expand Down Expand Up @@ -121,7 +127,7 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
const url = rewriteUrlForCodex(originalUrl);

// Step 3: Transform request body with Codex instructions
const transformation = transformRequestForCodex(init, url, CODEX_INSTRUCTIONS, userConfig);
const transformation = transformRequestForCodex(init, url, CODEX_INSTRUCTIONS, userConfig, codexMode);
const hasTools = transformation?.body.tools !== undefined;
const requestInit = transformation?.updatedInit ?? init;

Expand Down
2 changes: 1 addition & 1 deletion lib/auth.ts β†’ lib/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { generatePKCE } from "@openauthjs/openauth/pkce";
import { randomBytes } from "node:crypto";
import type { PKCEPair, AuthorizationFlow, TokenResult, ParsedAuthInput, JWTPayload } from "./types.js";
import type { PKCEPair, AuthorizationFlow, TokenResult, ParsedAuthInput, JWTPayload } from "../types.js";

// OAuth constants (from openai/codex)
export const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
Expand Down
2 changes: 1 addition & 1 deletion lib/browser.ts β†’ lib/auth/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { spawn } from "node:child_process";
import { PLATFORM_OPENERS } from "./constants.js";
import { PLATFORM_OPENERS } from "../constants.js";

/**
* Gets the platform-specific command to open a URL in the default browser
Expand Down
6 changes: 3 additions & 3 deletions lib/server.ts β†’ lib/auth/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { OAuthServerInfo } from "./types.js";
import type { OAuthServerInfo } from "../types.js";

// Resolve path to oauth-success.html
// Resolve path to oauth-success.html (one level up from auth/ subfolder)
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const successHtml = fs.readFileSync(path.join(__dirname, "oauth-success.html"), "utf-8");
const successHtml = fs.readFileSync(path.join(__dirname, "..", "oauth-success.html"), "utf-8");

/**
* Start a small local HTTP server that waits for /auth/callback and returns the code
Expand Down
60 changes: 60 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import type { PluginConfig } from "./types.js";

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

/**
* Default plugin configuration
* CODEX_MODE is enabled by default for better Codex CLI parity
*/
const DEFAULT_CONFIG: PluginConfig = {
codexMode: true,
};

/**
* Load plugin configuration from ~/.opencode/openai-codex-auth-config.json
* Falls back to defaults if file doesn't exist or is invalid
*
* @returns Plugin configuration
*/
export function loadPluginConfig(): PluginConfig {
try {
if (!existsSync(CONFIG_PATH)) {
return DEFAULT_CONFIG;
}

const fileContent = readFileSync(CONFIG_PATH, "utf-8");
const userConfig = JSON.parse(fileContent) as Partial<PluginConfig>;

// Merge with defaults
return {
...DEFAULT_CONFIG,
...userConfig,
};
} catch (error) {
console.warn(
`[openai-codex-plugin] Failed to load config from ${CONFIG_PATH}:`,
(error as Error).message
);
return DEFAULT_CONFIG;
}
}

/**
* Get the effective CODEX_MODE setting
* Priority: environment variable > config file > default (true)
*
* @param pluginConfig - Plugin configuration from file
* @returns True if CODEX_MODE should be enabled
*/
export function getCodexMode(pluginConfig: PluginConfig): boolean {
// Environment variable takes precedence
if (process.env.CODEX_MODE !== undefined) {
return process.env.CODEX_MODE === "1";
}

// Use config setting (defaults to true)
return pluginConfig.codexMode ?? true;
}
117 changes: 117 additions & 0 deletions lib/prompts/codex-opencode-bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Codex-OpenCode Bridge Prompt
*
* This prompt bridges Codex CLI instructions to the OpenCode environment.
* It incorporates critical tool mappings, available tools list, substitution rules,
* and verification checklist to ensure proper tool usage.
*
* Token Count: ~450 tokens (~90% reduction vs full OpenCode prompt)
*/

export const CODEX_OPENCODE_BRIDGE = `# Codex Running in OpenCode

You are running Codex through OpenCode, an open-source terminal coding assistant. OpenCode provides different tools but follows Codex operating principles.

## CRITICAL: Tool Replacements

<critical_rule priority="0">
❌ APPLY_PATCH DOES NOT EXIST β†’ βœ… USE "edit" INSTEAD
- NEVER use: apply_patch, applyPatch
- ALWAYS use: edit tool for ALL file modifications
- Before modifying files: Verify you're using "edit", NOT "apply_patch"
</critical_rule>

<critical_rule priority="0">
❌ UPDATE_PLAN DOES NOT EXIST β†’ βœ… USE "todowrite" INSTEAD
- NEVER use: update_plan, updatePlan, read_plan, readPlan
- ALWAYS use: todowrite for task/plan updates, todoread to read plans
- Before plan operations: Verify you're using "todowrite", NOT "update_plan"
</critical_rule>

## Available OpenCode Tools

**File Operations:**
- \`write\` - Create new files
- \`edit\` - Modify existing files (REPLACES apply_patch)
- \`read\` - Read file contents

**Search/Discovery:**
- \`grep\` - Search file contents
- \`glob\` - Find files by pattern
- \`ls\` - List directories (use relative paths)

**Execution:**
- \`bash\` - Run shell commands

**Network:**
- \`webfetch\` - Fetch web content

**Task Management:**
- \`todowrite\` - Manage tasks/plans (REPLACES update_plan)
- \`todoread\` - Read current plan

## Substitution Rules

Base instruction says: You MUST use instead:
apply_patch β†’ edit
update_plan β†’ todowrite
read_plan β†’ todoread
absolute paths β†’ relative paths

## Verification Checklist

Before file/plan modifications:
1. Am I using "edit" NOT "apply_patch"?
2. Am I using "todowrite" NOT "update_plan"?
3. Is this tool in the approved list above?
4. Am I using relative paths?

If ANY answer is NO β†’ STOP and correct before proceeding.

## OpenCode Working Style

**Communication:**
- Send brief preambles (8-12 words) before tool calls, building on prior context
- Provide progress updates during longer tasks

**Execution:**
- Keep working autonomously until query is fully resolved before yielding
- Don't return to user with partial solutions

**Code Approach:**
- New projects: Be ambitious and creative
- Existing codebases: Surgical precision - modify only what's requested

**Testing:**
- If tests exist: Start specific to your changes, then broader validation

## What Remains from Codex

Sandbox policies, approval mechanisms, final answer formatting, git commit protocols, and file reference formats all follow Codex instructions.`;

export interface CodexOpenCodeBridgeMeta {
estimatedTokens: number;
reductionVsCurrent: string;
reductionVsToolRemap: string;
protects: string[];
omits: string[];
}

export const CODEX_OPENCODE_BRIDGE_META: CodexOpenCodeBridgeMeta = {
estimatedTokens: 450,
reductionVsCurrent: "90%",
reductionVsToolRemap: "20%",
protects: [
"Tool name confusion (apply_patch/update_plan)",
"Missing tool awareness",
"Premature yielding to user",
"Over-modification of existing code",
"Environment confusion",
],
omits: [
"Sandbox details (in Codex)",
"Formatting rules (in Codex)",
"Tool schemas (in tool JSONs)",
"Git protocols (in Codex)",
],
};
2 changes: 1 addition & 1 deletion lib/codex.ts β†’ lib/prompts/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { homedir } from "node:os";
import type { GitHubRelease, CacheMetadata } from "./types.js";
import type { GitHubRelease, CacheMetadata } from "../types.js";

// Codex instructions constants
const GITHUB_API_RELEASES = "https://api.github.com/repos/openai/codex/releases/latest";
Expand Down
15 changes: 9 additions & 6 deletions lib/fetch-helpers.ts β†’ lib/request/fetch-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

import type { Auth } from "@opencode-ai/sdk";
import type { OpencodeClient } from "@opencode-ai/sdk";
import { refreshAccessToken } from "./auth.js";
import { logRequest } from "./logger.js";
import { refreshAccessToken } from "../auth/auth.js";
import { logRequest } from "../logger.js";
import { transformRequestBody } from "./request-transformer.js";
import { convertSseToJson, ensureContentType } from "./response-handler.js";
import type { UserConfig, RequestBody } from "./types.js";
import type { UserConfig, RequestBody } from "../types.js";
import {
PLUGIN_NAME,
HTTP_STATUS,
Expand All @@ -18,7 +18,7 @@ import {
URL_PATHS,
ERROR_MESSAGES,
LOG_STAGES,
} from "./constants.js";
} from "../constants.js";

/**
* Determines if the current auth token needs to be refreshed
Expand Down Expand Up @@ -104,13 +104,15 @@ export function rewriteUrlForCodex(url: string): string {
* @param url - Request URL
* @param codexInstructions - Codex system instructions
* @param userConfig - User configuration
* @param codexMode - Enable CODEX_MODE (bridge prompt instead of tool remap)
* @returns Transformed body and updated init, or undefined if no body
*/
export function transformRequestForCodex(
init: RequestInit | undefined,
url: string,
codexInstructions: string,
userConfig: UserConfig
userConfig: UserConfig,
codexMode: boolean = true
): { body: RequestBody; updatedInit: RequestInit } | undefined {
if (!init?.body) return undefined;

Expand All @@ -126,11 +128,12 @@ export function transformRequestForCodex(
hasTools: !!body.tools,
hasInput: !!body.input,
inputLength: body.input?.length,
codexMode,
body: body as unknown as Record<string, unknown>,
});

// Transform request body
const transformedBody = transformRequestBody(body, codexInstructions, userConfig);
const transformedBody = transformRequestBody(body, codexInstructions, userConfig, codexMode);

// Log transformed request
logRequest(LOG_STAGES.AFTER_TRANSFORM, {
Expand Down
Loading