Files
Numman AliandHanzo Dev 3890872781 feat: Add CODEX_MODE with bridge prompt and reorganize lib/ structure
## CODEX_MODE Implementation

Added configurable CODEX_MODE that replaces OpenCode system prompts with
a Codex-OpenCode bridge prompt for better Codex CLI parity.

**Key Features:**
- Enabled by default for optimal Codex CLI compatibility
- Configurable via ~/.opencode/openai-codex-auth-config.json
- Environment variable override support (CODEX_MODE=0 or CODEX_MODE=1)
- Priority: env var > config file > default (true)

**Bridge Prompt:**
- ~450 tokens (~90% reduction vs full OpenCode prompt)
- Critical tool replacements (apply_patch → edit, update_plan → todowrite)
- Available tools list and verification checklist
- OpenCode working style guidelines

**Configuration:**
```json
{
  "codexMode": true  // default
}
```

## Library Reorganization

Restructured lib/ into semantic subfolders:

```
lib/
├── auth/              # OAuth authentication
│   ├── auth.ts
│   ├── browser.ts
│   └── server.ts
├── prompts/           # System prompts
│   ├── codex.ts
│   └── codex-opencode-bridge.ts
├── request/           # Request handling
│   ├── fetch-helpers.ts
│   ├── request-transformer.ts
│   └── response-handler.ts
├── config.ts          # Plugin configuration
├── constants.ts
├── logger.ts
├── types.ts
└── oauth-success.html
```

## Files Changed

**New Files:**
- lib/config.ts - Plugin configuration loading
- lib/prompts/codex-opencode-bridge.ts - Bridge prompt
- test/plugin-config.test.ts - 12 new tests

**Modified:**
- lib/request/request-transformer.ts - CODEX_MODE parameter
- lib/request/fetch-helpers.ts - Pass codexMode through pipeline
- index.ts - Load config and determine codexMode
- lib/types.ts - Add PluginConfig type
- README.md - Document CODEX_MODE configuration

**Reorganized:**
- lib/*.ts → lib/{auth,prompts,request}/*.ts
- Updated all imports across codebase and tests

## Testing

- 123 total tests (added 12 new tests for config loading)
- All tests passing
- Comprehensive CODEX_MODE coverage:
  - Config file loading (exists, missing, invalid)
  - Priority order testing
  - Environment variable override
  - Bridge prompt vs tool remap behavior

## Documentation

Updated README.md:
- Added "Plugin Configuration" section
- Documented CODEX_MODE setting and priority
- Updated features list (123 tests, CODEX_MODE)
- Updated "How It Works" section

Co-authored-by: Hanzo Dev <[email protected]>
2025-10-03 22:31:34 +01:00

102 lines
2.8 KiB
TypeScript

import { logRequest, LOGGING_ENABLED } from "../logger.js";
import type { SSEEventData } from "../types.js";
/**
* Parse SSE stream to extract final response
* @param sseText - Complete SSE stream text
* @returns Final response object or null if not found
*/
function parseSseStream(sseText: string): unknown | null {
const lines = sseText.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.substring(6)) as SSEEventData;
// Look for response.done event with final data
if (data.type === 'response.done' || data.type === 'response.completed') {
return data.response;
}
} catch (e) {
// Skip malformed JSON
}
}
}
return null;
}
/**
* Convert SSE stream response to JSON for generateText()
* @param response - Fetch response with SSE stream
* @param headers - Response headers
* @returns Response with JSON body
*/
export async function convertSseToJson(response: Response, headers: Headers): Promise<Response> {
if (!response.body) {
throw new Error('[openai-codex-plugin] Response has no body');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullText = '';
try {
// Consume the entire stream
while (true) {
const { done, value } = await reader.read();
if (done) break;
fullText += decoder.decode(value, { stream: true });
}
if (LOGGING_ENABLED) {
logRequest("stream-full", { fullContent: fullText });
}
// Parse SSE events to extract the final response
const finalResponse = parseSseStream(fullText);
if (!finalResponse) {
console.error('[openai-codex-plugin] Could not find final response in SSE stream');
logRequest("stream-error", { error: "No response.done event found" });
// Return original stream if we can't parse
return new Response(fullText, {
status: response.status,
statusText: response.statusText,
headers: headers,
});
}
// Return as plain JSON (not SSE)
const jsonHeaders = new Headers(headers);
jsonHeaders.set('content-type', 'application/json; charset=utf-8');
return new Response(JSON.stringify(finalResponse), {
status: response.status,
statusText: response.statusText,
headers: jsonHeaders,
});
} catch (error) {
console.error('[openai-codex-plugin] Error converting stream:', error);
logRequest("stream-error", { error: String(error) });
throw error;
}
}
/**
* Ensure response has content-type header
* @param headers - Response headers
* @returns Headers with content-type set
*/
export function ensureContentType(headers: Headers): Headers {
const responseHeaders = new Headers(headers);
if (!responseHeaders.has('content-type')) {
responseHeaders.set('content-type', 'text/event-stream; charset=utf-8');
}
return responseHeaders;
}