codex-lens 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,140 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ const DEFAULT_LOG_DIR = join(homedir(), '.codex-viewer');
6
+
7
+ export class LogManager {
8
+ constructor(logDir = DEFAULT_LOG_DIR) {
9
+ this.logDir = logDir;
10
+ this.ensureLogDir();
11
+ }
12
+
13
+ ensureLogDir() {
14
+ if (!existsSync(this.logDir)) {
15
+ mkdirSync(this.logDir, { recursive: true });
16
+ }
17
+ }
18
+
19
+ getLogFilePath(projectName) {
20
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
21
+ return join(this.logDir, `${projectName}_${timestamp}.jsonl`);
22
+ }
23
+
24
+ appendEntry(logFile, entry) {
25
+ try {
26
+ const dir = dirname(logFile);
27
+ if (!existsSync(dir)) {
28
+ mkdirSync(dir, { recursive: true });
29
+ }
30
+ appendFileSync(logFile, JSON.stringify(entry) + '\n');
31
+ } catch (error) {
32
+ console.error('[LogManager] Failed to append entry:', error.message);
33
+ }
34
+ }
35
+
36
+ readLogFile(logFile) {
37
+ if (!existsSync(logFile)) {
38
+ return [];
39
+ }
40
+
41
+ try {
42
+ const content = readFileSync(logFile, 'utf-8');
43
+ const lines = content.split('\n').filter(l => l.trim());
44
+ return lines.map(line => {
45
+ try {
46
+ return JSON.parse(line);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }).filter(Boolean);
51
+ } catch (error) {
52
+ console.error('[LogManager] Failed to read log file:', error.message);
53
+ return [];
54
+ }
55
+ }
56
+
57
+ listLogFiles(projectName) {
58
+ try {
59
+ if (!existsSync(this.logDir)) {
60
+ return [];
61
+ }
62
+
63
+ const files = readdirSync(this.logDir)
64
+ .filter(f => f.startsWith(projectName + '_') && f.endsWith('.jsonl'))
65
+ .map(f => {
66
+ const stats = statSync(join(this.logDir, f));
67
+ return {
68
+ name: f,
69
+ path: join(this.logDir, f),
70
+ size: stats.size,
71
+ modified: stats.mtime
72
+ };
73
+ })
74
+ .sort((a, b) => b.modified - a.modified);
75
+
76
+ return files;
77
+ } catch (error) {
78
+ console.error('[LogManager] Failed to list log files:', error.message);
79
+ return [];
80
+ }
81
+ }
82
+
83
+ deleteOldLogs(projectName, keepCount = 10) {
84
+ try {
85
+ const files = this.listLogFiles(projectName);
86
+ const toDelete = files.slice(keepCount);
87
+
88
+ for (const file of toDelete) {
89
+ unlinkSync(file.path);
90
+ console.log(`[LogManager] Deleted old log: ${file.name}`);
91
+ }
92
+ } catch (error) {
93
+ console.error('[LogManager] Failed to delete old logs:', error.message);
94
+ }
95
+ }
96
+
97
+ getRecentLog(projectName) {
98
+ const files = this.listLogFiles(projectName);
99
+ if (files.length > 0) {
100
+ return files[0];
101
+ }
102
+ return null;
103
+ }
104
+
105
+ writeSessionMeta(logFile, meta) {
106
+ this.appendEntry(logFile, {
107
+ type: 'session_meta',
108
+ timestamp: new Date().toISOString(),
109
+ ...meta
110
+ });
111
+ }
112
+
113
+ writeApiRequest(logFile, request) {
114
+ this.appendEntry(logFile, {
115
+ type: 'api_request',
116
+ timestamp: new Date().toISOString(),
117
+ ...request
118
+ });
119
+ }
120
+
121
+ writeApiResponse(logFile, response) {
122
+ this.appendEntry(logFile, {
123
+ type: 'api_response',
124
+ timestamp: new Date().toISOString(),
125
+ ...response
126
+ });
127
+ }
128
+
129
+ writeFileChange(logFile, change) {
130
+ this.appendEntry(logFile, {
131
+ type: 'file_change',
132
+ timestamp: new Date().toISOString(),
133
+ ...change
134
+ });
135
+ }
136
+ }
137
+
138
+ export function createLogManager(logDir) {
139
+ return new LogManager(logDir);
140
+ }
@@ -0,0 +1,88 @@
1
+ import { appendFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { dirname, join, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const LOG_DIR = resolve(__dirname, '../../logs');
8
+
9
+ const LEVELS = {
10
+ DEBUG: 0,
11
+ INFO: 1,
12
+ WARN: 2,
13
+ ERROR: 3,
14
+ };
15
+
16
+ let sharedLogFile = null;
17
+ let sharedInitDone = false;
18
+
19
+ class Logger {
20
+ constructor(moduleName, level = 'INFO') {
21
+ this.moduleName = moduleName;
22
+ this.level = LEVELS[level] ?? LEVELS.INFO;
23
+ }
24
+
25
+ init() {
26
+ if (!sharedInitDone) {
27
+ if (!existsSync(LOG_DIR)) {
28
+ mkdirSync(LOG_DIR, { recursive: true });
29
+ }
30
+
31
+ const now = new Date();
32
+ const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
33
+ sharedLogFile = join(LOG_DIR, `${timestamp}.txt`);
34
+
35
+ appendFileSync(sharedLogFile, `\n========== Session Started: ${now.toISOString()} ==========\n`);
36
+ sharedInitDone = true;
37
+ }
38
+ }
39
+
40
+ _format(level, message) {
41
+ const now = new Date();
42
+ const timestamp = now.toISOString();
43
+ return `[${timestamp}] [${level}] [${this.moduleName}] ${message}`;
44
+ }
45
+
46
+ _write(level, message) {
47
+ if (LEVELS[level] < this.level) return;
48
+
49
+ const formatted = this._format(level, message);
50
+
51
+ console.log(formatted);
52
+
53
+ if (sharedLogFile) {
54
+ try {
55
+ appendFileSync(sharedLogFile, formatted + '\n');
56
+ } catch (e) {
57
+ console.error('Failed to write to log file:', e);
58
+ }
59
+ }
60
+ }
61
+
62
+ debug(message) {
63
+ this._write('DEBUG', message);
64
+ }
65
+
66
+ info(message) {
67
+ this._write('INFO', message);
68
+ }
69
+
70
+ warn(message) {
71
+ this._write('WARN', message);
72
+ }
73
+
74
+ error(message) {
75
+ this._write('ERROR', message);
76
+ }
77
+
78
+ errorWithStack(message, error) {
79
+ const stack = error?.stack || error?.message || error || '';
80
+ this._write('ERROR', `${message}\n ${stack}`);
81
+ }
82
+ }
83
+
84
+ export function createLogger(moduleName, level = 'INFO') {
85
+ const logger = new Logger(moduleName, level);
86
+ logger.init();
87
+ return logger;
88
+ }
@@ -0,0 +1,79 @@
1
+ export function parseSSEStream(data) {
2
+ if (!data || typeof data !== 'string') {
3
+ return null;
4
+ }
5
+
6
+ const lines = data.split('\n');
7
+ const event = {
8
+ type: null,
9
+ data: null,
10
+ id: null,
11
+ retry: null
12
+ };
13
+
14
+ for (const line of lines) {
15
+ if (line.startsWith('event:')) {
16
+ event.type = line.slice(6).trim();
17
+ } else if (line.startsWith('data:')) {
18
+ const value = line.slice(5).trim();
19
+ try {
20
+ event.data = JSON.parse(value);
21
+ } catch {
22
+ event.data = value;
23
+ }
24
+ } else if (line.startsWith('id:')) {
25
+ event.id = line.slice(3).trim();
26
+ } else if (line.startsWith('retry:')) {
27
+ event.retry = parseInt(line.slice(7).trim(), 10);
28
+ }
29
+ }
30
+
31
+ if (event.type === 'null' && !event.data) {
32
+ return null;
33
+ }
34
+
35
+ return event;
36
+ }
37
+
38
+ export function parseSSELines(lines) {
39
+ const events = [];
40
+
41
+ for (const line of lines) {
42
+ const event = parseSSEStream(line);
43
+ if (event) {
44
+ events.push(event);
45
+ }
46
+ }
47
+
48
+ return events;
49
+ }
50
+
51
+ export function extractDelta(event) {
52
+ if (!event || !event.data) return null;
53
+
54
+ if (event.data.type === 'content_block_delta') {
55
+ return event.data.delta;
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ export function extractToolUse(event) {
62
+ if (!event || !event.data) return null;
63
+
64
+ if (event.data.type === 'tool_use') {
65
+ return event.data;
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ export function extractMessage(event) {
72
+ if (!event || !event.data) return null;
73
+
74
+ if (event.data.type === 'message') {
75
+ return event.data.message;
76
+ }
77
+
78
+ return null;
79
+ }
package/src/main.jsx ADDED
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { App } from './components/App.jsx';
4
+ import './global.css';
5
+
6
+ const container = document.getElementById('root');
7
+ const root = createRoot(container);
8
+
9
+ root.render(<App />);
package/src/proxy.js ADDED
@@ -0,0 +1,156 @@
1
+ import { createServer } from 'http';
2
+ import { parseSSEStream } from './lib/sse-parser.js';
3
+ import { createLogger } from './lib/logger.js';
4
+
5
+ const DEFAULT_UPSTREAM = 'https://api.openai.com';
6
+ const PROXY_PORT = 8080;
7
+
8
+ const logger = createLogger('Proxy');
9
+
10
+ export class ProxyServer {
11
+ constructor(wsEmitter, port = PROXY_PORT) {
12
+ this.port = port;
13
+ this.wsEmitter = wsEmitter;
14
+ this.server = null;
15
+ }
16
+
17
+ async start() {
18
+ return new Promise((resolve) => {
19
+ this.server = createServer(async (req, res) => {
20
+ const baseUrl = process.env.OPENAI_BASE_URL || DEFAULT_UPSTREAM;
21
+ const url = new URL(req.url, baseUrl);
22
+ const upstreamUrl = `${baseUrl}${url.pathname}${url.search}`;
23
+
24
+ logger.info(`Incoming request: ${req.method} ${upstreamUrl}`);
25
+
26
+ const headers = { ...req.headers };
27
+ delete headers.host;
28
+
29
+ const chunks = [];
30
+ for await (const chunk of req) {
31
+ chunks.push(chunk);
32
+ }
33
+ const body = chunks.length > 0 ? Buffer.concat(chunks) : null;
34
+
35
+ const requestData = {
36
+ timestamp: new Date().toISOString(),
37
+ method: req.method,
38
+ url: upstreamUrl,
39
+ headers: headers,
40
+ body: body ? JSON.parse(body.toString()) : null,
41
+ };
42
+
43
+ try {
44
+ logger.debug(`Forwarding to upstream: ${upstreamUrl}`);
45
+
46
+ const upstreamRes = await fetch(upstreamUrl, {
47
+ method: req.method,
48
+ headers: headers,
49
+ body: body,
50
+ });
51
+
52
+ this.wsEmitter({
53
+ type: 'api_request',
54
+ data: requestData,
55
+ });
56
+
57
+ const responseHeaders = {};
58
+ for (const [key, value] of upstreamRes.headers.entries()) {
59
+ if (key.toLowerCase() !== 'content-encoding' &&
60
+ key.toLowerCase() !== 'transfer-encoding') {
61
+ responseHeaders[key] = value;
62
+ }
63
+ }
64
+
65
+ const contentType = upstreamRes.headers.get('content-type') || '';
66
+ const isStreaming = contentType.includes('text/event-stream') ||
67
+ contentType.includes('stream');
68
+
69
+ if (isStreaming) {
70
+ logger.debug('Streaming response detected');
71
+ res.writeHead(upstreamRes.status, responseHeaders);
72
+
73
+ const clonedRes = upstreamRes.body.clone();
74
+
75
+ this.parseStream(clonedRes, (event) => {
76
+ this.wsEmitter({ type: 'sse_event', data: event });
77
+ });
78
+
79
+ upstreamRes.body.pipe(res);
80
+ } else {
81
+ const responseBody = await upstreamRes.text();
82
+
83
+ this.wsEmitter({
84
+ type: 'api_response',
85
+ data: {
86
+ timestamp: new Date().toISOString(),
87
+ status: upstreamRes.status,
88
+ headers: responseHeaders,
89
+ body: responseBody,
90
+ },
91
+ });
92
+
93
+ logger.info(`Response received: ${upstreamRes.status}`);
94
+
95
+ res.writeHead(upstreamRes.status, responseHeaders);
96
+ res.end(responseBody);
97
+ }
98
+ } catch (error) {
99
+ logger.errorWithStack('Proxy error:', error);
100
+ res.writeHead(502, { 'Content-Type': 'application/json' });
101
+ res.end(JSON.stringify({ error: error.message }));
102
+ }
103
+ });
104
+
105
+ this.server.listen(this.port, () => {
106
+ logger.info(`Proxy server started on port ${this.port}`);
107
+ resolve(this.port);
108
+ });
109
+ });
110
+ }
111
+
112
+ parseStream(stream, onEvent) {
113
+ const decoder = new TextDecoder();
114
+ let buffer = '';
115
+
116
+ const reader = stream.getReader();
117
+
118
+ const read = () => {
119
+ reader.read().then(({ done, value }) => {
120
+ if (done) {
121
+ if (buffer.trim()) {
122
+ const event = parseSSEStream(buffer);
123
+ if (event) onEvent(event);
124
+ }
125
+ return;
126
+ }
127
+
128
+ buffer += decoder.decode(value, { stream: true });
129
+ const lines = buffer.split('\n');
130
+ buffer = lines.pop() || '';
131
+
132
+ for (const line of lines) {
133
+ if (line.trim()) {
134
+ const event = parseSSEStream(line);
135
+ if (event) onEvent(event);
136
+ }
137
+ }
138
+
139
+ read();
140
+ });
141
+ };
142
+
143
+ read();
144
+ }
145
+
146
+ stop() {
147
+ if (this.server) {
148
+ this.server.close();
149
+ logger.info('Proxy server stopped');
150
+ }
151
+ }
152
+ }
153
+
154
+ export function createProxyServer(wsEmitter, port) {
155
+ return new ProxyServer(wsEmitter, port);
156
+ }
@@ -0,0 +1,183 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { join, dirname } from 'node:path';
3
+ import { platform, arch } from 'node:os';
4
+ import { chmodSync, statSync } from 'node:fs';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ let ptyProcess = null;
10
+ let dataListeners = [];
11
+ let exitListeners = [];
12
+ let lastExitCode = null;
13
+ let outputBuffer = '';
14
+ let lastPtyCols = 120;
15
+ let lastPtyRows = 30;
16
+ const MAX_BUFFER = 200000;
17
+ let batchBuffer = '';
18
+ let batchScheduled = false;
19
+
20
+ async function getPty() {
21
+ const ptyMod = await import('node-pty');
22
+ return ptyMod.default || ptyMod;
23
+ }
24
+
25
+ function findSafeSliceStart(buf, rawStart) {
26
+ const scanLimit = Math.min(rawStart + 64, buf.length);
27
+ let i = rawStart;
28
+ while (i < scanLimit) {
29
+ const ch = buf.charCodeAt(i);
30
+ if (ch === 0x1b) {
31
+ let j = i + 1;
32
+ while (j < scanLimit && !((buf.charCodeAt(j) >= 0x40 && buf.charCodeAt(j) <= 0x7e) && j > i + 1)) {
33
+ j++;
34
+ }
35
+ if (j < scanLimit) {
36
+ return j + 1;
37
+ }
38
+ i = j;
39
+ continue;
40
+ }
41
+ if ((ch >= 0x20 && ch <= 0x3f)) {
42
+ i++;
43
+ continue;
44
+ }
45
+ break;
46
+ }
47
+ return i < buf.length ? i : rawStart;
48
+ }
49
+
50
+ function flushBatch() {
51
+ batchScheduled = false;
52
+ if (!batchBuffer) return;
53
+ const chunk = batchBuffer;
54
+ batchBuffer = '';
55
+ for (const cb of dataListeners) {
56
+ try { cb(chunk); } catch { }
57
+ }
58
+ }
59
+
60
+ function fixSpawnHelperPermissions() {
61
+ try {
62
+ const os = platform();
63
+ const cpu = arch();
64
+ const helperPath = join(__dirname, 'node_modules', 'node-pty', 'prebuilds', `${os}-${cpu}`, 'spawn-helper');
65
+ const stat = statSync(helperPath);
66
+ if (!(stat.mode & 0o111)) {
67
+ chmodSync(helperPath, stat.mode | 0o755);
68
+ }
69
+ } catch { }
70
+ }
71
+
72
+ export async function spawnCodex(codexBinary, projectRoot, proxyPort) {
73
+ if (ptyProcess) {
74
+ killPty();
75
+ }
76
+
77
+ const pty = await getPty();
78
+
79
+ fixSpawnHelperPermissions();
80
+
81
+ const shell = platform() === 'win32' ? 'powershell.exe' : 'bash';
82
+ const args = platform() === 'win32'
83
+ ? ['-NoExit', '-Command', `Set-Location "${projectRoot}"; & "${codexBinary}"`]
84
+ : [];
85
+
86
+ const env = { ...process.env };
87
+ env.OPENAI_BASE_URL = `http://127.0.0.1:${proxyPort}`;
88
+ if (platform() === 'win32') {
89
+ env.WINPTY = '1';
90
+ }
91
+
92
+ lastExitCode = null;
93
+ outputBuffer = '';
94
+
95
+ ptyProcess = pty.spawn(shell, args, {
96
+ name: 'xterm-256color',
97
+ cols: lastPtyCols,
98
+ rows: lastPtyRows,
99
+ cwd: projectRoot,
100
+ env,
101
+ useConpty: false,
102
+ });
103
+
104
+ ptyProcess.onData((data) => {
105
+ outputBuffer += data;
106
+ if (outputBuffer.length > MAX_BUFFER) {
107
+ const rawStart = outputBuffer.length - MAX_BUFFER;
108
+ const safeStart = findSafeSliceStart(outputBuffer, rawStart);
109
+ outputBuffer = outputBuffer.slice(safeStart);
110
+ }
111
+ batchBuffer += data;
112
+ if (!batchScheduled) {
113
+ batchScheduled = true;
114
+ setImmediate(flushBatch);
115
+ }
116
+ });
117
+
118
+ ptyProcess.onExit(({ exitCode }) => {
119
+ flushBatch();
120
+ lastExitCode = exitCode;
121
+ ptyProcess = null;
122
+ for (const cb of exitListeners) {
123
+ try { cb(exitCode); } catch { }
124
+ }
125
+ });
126
+
127
+ return ptyProcess;
128
+ }
129
+
130
+ export function writeToPty(data) {
131
+ if (ptyProcess) {
132
+ ptyProcess.write(data);
133
+ return true;
134
+ }
135
+ return false;
136
+ }
137
+
138
+ export function resizePty(cols, rows) {
139
+ lastPtyCols = cols;
140
+ lastPtyRows = rows;
141
+ if (ptyProcess) {
142
+ try { ptyProcess.resize(cols, rows); } catch { }
143
+ }
144
+ }
145
+
146
+ export function killPty() {
147
+ if (ptyProcess) {
148
+ flushBatch();
149
+ batchBuffer = '';
150
+ batchScheduled = false;
151
+ try { ptyProcess.kill(); } catch { }
152
+ ptyProcess = null;
153
+ }
154
+ }
155
+
156
+ export function onPtyData(cb) {
157
+ dataListeners.push(cb);
158
+ return () => {
159
+ dataListeners = dataListeners.filter(l => l !== cb);
160
+ };
161
+ }
162
+
163
+ export function onPtyExit(cb) {
164
+ exitListeners.push(cb);
165
+ return () => {
166
+ exitListeners = exitListeners.filter(l => l !== cb);
167
+ };
168
+ }
169
+
170
+ export function getPtyPid() {
171
+ return ptyProcess ? ptyProcess.pid : null;
172
+ }
173
+
174
+ export function getPtyState() {
175
+ return {
176
+ running: !!ptyProcess,
177
+ exitCode: lastExitCode,
178
+ };
179
+ }
180
+
181
+ export function getOutputBuffer() {
182
+ return outputBuffer;
183
+ }