@galaxy-yearn/codex-deepseek-gateway 0.1.5 → 0.1.7
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.
- package/README.md +42 -29
- package/bin/codex-deepseek-gateway.js +47 -11
- package/config/codex-model-catalog.json +130 -0
- package/config/codex-model-catalog.zh.json +130 -0
- package/config/frontend-design-guidance/en.md +33 -0
- package/config/frontend-design-guidance/zh.md +33 -0
- package/config/gateway.example.json +1 -0
- package/package.json +4 -1
- package/src/codex-launch.js +250 -45
- package/src/codex-sessions.js +19 -7
- package/src/config.js +4 -1
- package/src/local-config.js +6 -2
- package/src/prompt-language.js +13 -0
- package/src/protocol.js +328 -63
- package/src/server.js +3 -0
package/src/codex-launch.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
4
|
import { dirname, join, resolve } from 'node:path';
|
|
4
5
|
import { clearScreenDown, cursorTo } from 'node:readline';
|
|
5
6
|
import { readCodexConfig } from './codex-config.js';
|
|
7
|
+
import { readLocalConfigFile } from './local-config.js';
|
|
8
|
+
import { catalogFileForPromptLanguage, normalizePromptLanguage } from './prompt-language.js';
|
|
6
9
|
|
|
7
10
|
export const DEFAULT_PROVIDER = 'deepseek-gateway';
|
|
8
11
|
export const REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
|
|
9
|
-
const SELECTED_ROW = '\x1b[38;
|
|
12
|
+
const SELECTED_ROW = '\x1b[38;2;57;100;254m';
|
|
10
13
|
const RESET_STYLE = '\x1b[0m';
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const CODEX_PLATFORM_PACKAGE_BY_TARGET = {
|
|
16
|
+
'x86_64-unknown-linux-musl': '@openai/codex-linux-x64',
|
|
17
|
+
'aarch64-unknown-linux-musl': '@openai/codex-linux-arm64',
|
|
18
|
+
'x86_64-apple-darwin': '@openai/codex-darwin-x64',
|
|
19
|
+
'aarch64-apple-darwin': '@openai/codex-darwin-arm64',
|
|
20
|
+
'x86_64-pc-windows-msvc': '@openai/codex-win32-x64',
|
|
21
|
+
'aarch64-pc-windows-msvc': '@openai/codex-win32-arm64',
|
|
22
|
+
};
|
|
11
23
|
|
|
12
24
|
function print(message = '') {
|
|
13
25
|
process.stdout.write(message);
|
|
@@ -37,16 +49,25 @@ export function gatewayModels(installDir) {
|
|
|
37
49
|
return Object.keys(readJsonObject(join(installDir, 'config', 'model-aliases.json'))).sort();
|
|
38
50
|
}
|
|
39
51
|
|
|
52
|
+
function readPromptLanguage(installDir) {
|
|
53
|
+
const configFile = join(installDir, 'config', 'gateway.local.json');
|
|
54
|
+
const localConfig = readLocalConfigFile(configFile);
|
|
55
|
+
return normalizePromptLanguage(localConfig.CODEX_PROMPT_LANGUAGE);
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
export function createLaunchContext(options) {
|
|
41
59
|
const codexHome = defaultCodexHome();
|
|
42
60
|
const models = gatewayModels(options.dir);
|
|
43
61
|
const codexConfig = readCodexConfig();
|
|
62
|
+
const promptLanguage = readPromptLanguage(options.dir);
|
|
44
63
|
const configModel = codexConfig.modelProvider === DEFAULT_PROVIDER && models.includes(codexConfig.model) ? codexConfig.model : '';
|
|
45
64
|
return {
|
|
46
65
|
all: options.all,
|
|
47
66
|
codexHome,
|
|
48
67
|
installDir: options.dir,
|
|
68
|
+
modelCatalogPath: join(options.dir, 'config', catalogFileForPromptLanguage(promptLanguage)),
|
|
49
69
|
models,
|
|
70
|
+
promptLanguage,
|
|
50
71
|
projectRoot: findProjectRoot(process.cwd()),
|
|
51
72
|
provider: options.provider || DEFAULT_PROVIDER,
|
|
52
73
|
model: options.model || configModel || models[0] || '',
|
|
@@ -59,7 +80,7 @@ export function missingModelMessage(context) {
|
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
function configOverrideArgs(context) {
|
|
62
|
-
|
|
83
|
+
const args = [
|
|
63
84
|
'-c',
|
|
64
85
|
`model_provider=${JSON.stringify(context.provider)}`,
|
|
65
86
|
'-c',
|
|
@@ -71,21 +92,10 @@ function configOverrideArgs(context) {
|
|
|
71
92
|
'-c',
|
|
72
93
|
'model_reasoning_summary="auto"',
|
|
73
94
|
];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return
|
|
78
|
-
'-c',
|
|
79
|
-
`model_provider=${context.provider}`,
|
|
80
|
-
'-c',
|
|
81
|
-
`model=${context.model}`,
|
|
82
|
-
'-c',
|
|
83
|
-
`model_reasoning_effort=${context.reasoningEffort}`,
|
|
84
|
-
'-c',
|
|
85
|
-
'model_supports_reasoning_summaries=true',
|
|
86
|
-
'-c',
|
|
87
|
-
'model_reasoning_summary=auto',
|
|
88
|
-
];
|
|
95
|
+
if (context.provider === DEFAULT_PROVIDER && context.modelCatalogPath) {
|
|
96
|
+
args.push('-c', `model_catalog_json=${JSON.stringify(context.modelCatalogPath)}`);
|
|
97
|
+
}
|
|
98
|
+
return args;
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
export function codexNewArgs(context) {
|
|
@@ -96,20 +106,146 @@ export function codexResumeArgs(sessionId, context) {
|
|
|
96
106
|
return ['resume', sessionId, ...configOverrideArgs(context)];
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
function quoteShellArg(arg) {
|
|
110
|
+
const value = String(arg);
|
|
111
|
+
if (/^[A-Za-z0-9_./:=@%+,-]+$/.test(value)) return value;
|
|
112
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
export function codexResumeCommand(sessionId, context) {
|
|
104
|
-
return ['codex',
|
|
116
|
+
return ['codex', ...codexResumeArgs(sessionId, context)].map(quoteShellArg).join(' ');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function codexTargetTriple(platform = process.platform, arch = process.arch) {
|
|
120
|
+
if ((platform === 'linux' || platform === 'android') && arch === 'x64') return 'x86_64-unknown-linux-musl';
|
|
121
|
+
if ((platform === 'linux' || platform === 'android') && arch === 'arm64') return 'aarch64-unknown-linux-musl';
|
|
122
|
+
if (platform === 'darwin' && arch === 'x64') return 'x86_64-apple-darwin';
|
|
123
|
+
if (platform === 'darwin' && arch === 'arm64') return 'aarch64-apple-darwin';
|
|
124
|
+
if (platform === 'win32' && arch === 'x64') return 'x86_64-pc-windows-msvc';
|
|
125
|
+
if (platform === 'win32' && arch === 'arm64') return 'aarch64-pc-windows-msvc';
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function pathEntries(env = process.env) {
|
|
130
|
+
const pathValue = env.PATH || env.Path || env.path || '';
|
|
131
|
+
return pathValue.split(process.platform === 'win32' ? ';' : ':').filter(Boolean);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function candidateExecutables(command, env = process.env) {
|
|
135
|
+
if (command.includes('/') || command.includes('\\')) return [command];
|
|
136
|
+
const extensions = process.platform === 'win32'
|
|
137
|
+
? (env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
|
|
138
|
+
: [''];
|
|
139
|
+
const names = process.platform === 'win32'
|
|
140
|
+
? extensions.map((extension) => `${command}${extension.toLowerCase()}`)
|
|
141
|
+
: [command];
|
|
142
|
+
return pathEntries(env).flatMap((entry) => names.map((name) => join(entry, name)));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function npmShimCodexPackageRoot(shimPath) {
|
|
146
|
+
const base = dirname(shimPath);
|
|
147
|
+
const packageRoot = join(base, 'node_modules', '@openai', 'codex');
|
|
148
|
+
return existsSync(join(packageRoot, 'package.json')) ? packageRoot : '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function ancestorCodexPackageRoot(filePath) {
|
|
152
|
+
try {
|
|
153
|
+
let current = dirname(realpathSync(filePath));
|
|
154
|
+
while (true) {
|
|
155
|
+
const packageJson = join(current, 'package.json');
|
|
156
|
+
if (existsSync(packageJson)) {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(readFileSync(packageJson, 'utf8'));
|
|
159
|
+
if (parsed?.name === '@openai/codex') return current;
|
|
160
|
+
} catch {
|
|
161
|
+
// Keep walking; this is only a best-effort executable locator.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const parent = dirname(current);
|
|
165
|
+
if (parent === current) return '';
|
|
166
|
+
current = parent;
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
return '';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function codexPackageRoot() {
|
|
174
|
+
try {
|
|
175
|
+
return dirname(require.resolve('@openai/codex/package.json'));
|
|
176
|
+
} catch {
|
|
177
|
+
for (const candidate of candidateExecutables('codex')) {
|
|
178
|
+
const packageRoot = npmShimCodexPackageRoot(candidate) || ancestorCodexPackageRoot(candidate);
|
|
179
|
+
if (packageRoot) return packageRoot;
|
|
180
|
+
}
|
|
181
|
+
return '';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function codexPlatformPackageJson(packageRoot, platformPackage) {
|
|
186
|
+
try {
|
|
187
|
+
return createRequire(join(packageRoot, 'package.json')).resolve(`${platformPackage}/package.json`);
|
|
188
|
+
} catch {
|
|
189
|
+
const nested = join(packageRoot, 'node_modules', platformPackage, 'package.json');
|
|
190
|
+
return existsSync(nested) ? nested : '';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function resolveCodexExecutable() {
|
|
195
|
+
const packageRoot = codexPackageRoot();
|
|
196
|
+
const targetTriple = codexTargetTriple();
|
|
197
|
+
const platformPackage = CODEX_PLATFORM_PACKAGE_BY_TARGET[targetTriple];
|
|
198
|
+
if (packageRoot && targetTriple && platformPackage) {
|
|
199
|
+
const packageJson = codexPlatformPackageJson(packageRoot, platformPackage);
|
|
200
|
+
if (existsSync(packageJson)) {
|
|
201
|
+
const executable = join(
|
|
202
|
+
dirname(packageJson),
|
|
203
|
+
'vendor',
|
|
204
|
+
targetTriple,
|
|
205
|
+
'bin',
|
|
206
|
+
process.platform === 'win32' ? 'codex.exe' : 'codex',
|
|
207
|
+
);
|
|
208
|
+
if (existsSync(executable)) return executable;
|
|
209
|
+
}
|
|
210
|
+
const bundledExecutable = join(
|
|
211
|
+
packageRoot,
|
|
212
|
+
'vendor',
|
|
213
|
+
targetTriple,
|
|
214
|
+
'bin',
|
|
215
|
+
process.platform === 'win32' ? 'codex.exe' : 'codex',
|
|
216
|
+
);
|
|
217
|
+
if (existsSync(bundledExecutable)) return bundledExecutable;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const candidate of candidateExecutables('codex')) {
|
|
221
|
+
if (existsSync(candidate) && !candidate.endsWith('.cmd') && !candidate.endsWith('.bat') && !candidate.endsWith('.ps1')) {
|
|
222
|
+
return candidate;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return 'codex';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function codexProcessEnv() {
|
|
229
|
+
const packageRoot = codexPackageRoot();
|
|
230
|
+
if (!packageRoot) return process.env;
|
|
231
|
+
return {
|
|
232
|
+
...process.env,
|
|
233
|
+
CODEX_MANAGED_BY_NPM: '1',
|
|
234
|
+
CODEX_MANAGED_PACKAGE_ROOT: realpathSync(packageRoot),
|
|
235
|
+
};
|
|
105
236
|
}
|
|
106
237
|
|
|
107
238
|
export async function runCodex(args) {
|
|
108
|
-
const child = spawn(
|
|
239
|
+
const child = spawn(resolveCodexExecutable(), args, {
|
|
109
240
|
stdio: 'inherit',
|
|
110
|
-
shell:
|
|
241
|
+
shell: false,
|
|
242
|
+
env: codexProcessEnv(),
|
|
111
243
|
});
|
|
112
244
|
process.exitCode = await new Promise((resolveChild) => {
|
|
245
|
+
child.on('error', (error) => {
|
|
246
|
+
process.stderr.write(`Failed to launch codex: ${error.message}\n`);
|
|
247
|
+
resolveChild(1);
|
|
248
|
+
});
|
|
113
249
|
child.on('exit', (exitCode) => resolveChild(exitCode ?? 0));
|
|
114
250
|
});
|
|
115
251
|
}
|
|
@@ -118,25 +254,73 @@ function rowOffset(header) {
|
|
|
118
254
|
return header ? 4 : 2;
|
|
119
255
|
}
|
|
120
256
|
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
const
|
|
257
|
+
export function pickerWindow(rows, selected, windowSize, currentOffset = 0) {
|
|
258
|
+
const allRows = Array.isArray(rows) ? rows : [];
|
|
259
|
+
const size = Math.max(1, Math.trunc(Number(windowSize) || allRows.length || 1));
|
|
260
|
+
const clampedSelected = Math.min(Math.max(0, selected), Math.max(0, allRows.length - 1));
|
|
261
|
+
const maxOffset = Math.max(0, allRows.length - size);
|
|
262
|
+
let offset = Math.min(Math.max(0, Math.trunc(Number(currentOffset) || 0)), maxOffset);
|
|
263
|
+
if (clampedSelected < offset) {
|
|
264
|
+
offset = clampedSelected;
|
|
265
|
+
} else if (clampedSelected >= offset + size) {
|
|
266
|
+
offset = clampedSelected - size + 1;
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
offset,
|
|
270
|
+
rows: allRows.slice(offset, offset + size),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function pickerControls(shortcuts = []) {
|
|
275
|
+
const labels = shortcuts.map((shortcut) => shortcut.label).filter(Boolean);
|
|
276
|
+
return [...labels, '↑/↓ select', '← back', 'Enter confirm', 'Esc quit'].join(' ');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function renderRow(state, absoluteIndex) {
|
|
280
|
+
if (absoluteIndex < state.offset || absoluteIndex >= state.offset + state.visibleRows.length) return;
|
|
281
|
+
const visibleIndex = absoluteIndex - state.offset;
|
|
282
|
+
cursorTo(process.stdout, 0, rowOffset(state.header) + visibleIndex);
|
|
283
|
+
const row = `${absoluteIndex === state.selected ? '>' : ' '} ${state.rows[absoluteIndex]}`;
|
|
284
|
+
const styled = absoluteIndex === state.selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row;
|
|
125
285
|
print(`\x1b[2K${styled}`);
|
|
126
286
|
}
|
|
127
287
|
|
|
128
|
-
function
|
|
129
|
-
const { title, rows, selected, header } = state;
|
|
288
|
+
function pickerLines(state) {
|
|
289
|
+
const { title, rows, selected, header, emptyMessage, shortcuts } = state;
|
|
290
|
+
const window = pickerWindow(rows, selected, state.windowSize, state.offset);
|
|
291
|
+
state.offset = window.offset;
|
|
292
|
+
state.visibleRows = window.rows;
|
|
293
|
+
const lines = [title, ''];
|
|
294
|
+
if (header) lines.push(` ${header}`, ` ${'-'.repeat(header.length)}`);
|
|
295
|
+
for (const [visibleIndex, rowText] of state.visibleRows.entries()) {
|
|
296
|
+
const absoluteIndex = state.offset + visibleIndex;
|
|
297
|
+
const row = `${absoluteIndex === selected ? '>' : ' '} ${rowText}`;
|
|
298
|
+
lines.push(absoluteIndex === selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row);
|
|
299
|
+
}
|
|
300
|
+
if (!rows.length && emptyMessage) lines.push(emptyMessage);
|
|
301
|
+
if (rows.length > state.visibleRows.length) {
|
|
302
|
+
lines.push('', ` Showing ${state.offset + 1}-${state.offset + state.visibleRows.length} of ${rows.length}`);
|
|
303
|
+
}
|
|
304
|
+
lines.push('', ` ${pickerControls(shortcuts)}`);
|
|
305
|
+
return lines;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function renderPicker(state, { clear = true } = {}) {
|
|
309
|
+
const lines = pickerLines(state);
|
|
130
310
|
cursorTo(process.stdout, 0, 0);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
311
|
+
if (clear) {
|
|
312
|
+
clearScreenDown(process.stdout);
|
|
313
|
+
print(`${lines.join('\n')}\n`);
|
|
314
|
+
state.renderedLineCount = lines.length;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
print(`\x1b[2K${line}\n`);
|
|
137
319
|
}
|
|
138
|
-
|
|
139
|
-
|
|
320
|
+
for (let index = lines.length; index < (state.renderedLineCount || 0); index += 1) {
|
|
321
|
+
print('\x1b[2K\n');
|
|
322
|
+
}
|
|
323
|
+
state.renderedLineCount = lines.length;
|
|
140
324
|
}
|
|
141
325
|
|
|
142
326
|
function openPickerScreen() {
|
|
@@ -147,11 +331,23 @@ function closePickerScreen() {
|
|
|
147
331
|
process.stdout.write('\x1b[?25h\x1b[?1049l');
|
|
148
332
|
}
|
|
149
333
|
|
|
150
|
-
export async function pick(title, rows, header = '') {
|
|
151
|
-
|
|
334
|
+
export async function pick(title, rows, header = '', options = {}) {
|
|
335
|
+
const shortcuts = Array.isArray(options.shortcuts) ? options.shortcuts : [];
|
|
336
|
+
if (!rows.length && !shortcuts.length) return { action: 'back' };
|
|
152
337
|
let selected = 0;
|
|
153
338
|
const stdin = process.stdin;
|
|
154
|
-
const state = {
|
|
339
|
+
const state = {
|
|
340
|
+
title,
|
|
341
|
+
rows,
|
|
342
|
+
selected,
|
|
343
|
+
header,
|
|
344
|
+
shortcuts,
|
|
345
|
+
windowSize: options.windowSize,
|
|
346
|
+
emptyMessage: options.emptyMessage,
|
|
347
|
+
offset: 0,
|
|
348
|
+
visibleRows: [],
|
|
349
|
+
renderedLineCount: 0,
|
|
350
|
+
};
|
|
155
351
|
renderPicker(state);
|
|
156
352
|
|
|
157
353
|
return await new Promise((resolvePick) => {
|
|
@@ -163,13 +359,25 @@ export async function pick(title, rows, header = '') {
|
|
|
163
359
|
const key = chunk.toString('utf8');
|
|
164
360
|
if (key === '\u0003' || key === '\u001b') return done({ action: 'cancel' });
|
|
165
361
|
if (key === '\u001b[D') return done({ action: 'back' });
|
|
166
|
-
|
|
362
|
+
for (const shortcut of shortcuts) {
|
|
363
|
+
if (key.toLowerCase() === String(shortcut.key || '').toLowerCase()) {
|
|
364
|
+
return done({ action: shortcut.action || shortcut.key });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if ((key === '\r' || key === '\n') && rows.length) return done({ action: 'select', index: selected });
|
|
368
|
+
if (!rows.length) return;
|
|
167
369
|
const previous = selected;
|
|
370
|
+
const previousOffset = state.offset;
|
|
168
371
|
if (key === '\u001b[A') selected = Math.max(0, selected - 1);
|
|
169
372
|
else if (key === '\u001b[B') selected = Math.min(rows.length - 1, selected + 1);
|
|
170
373
|
else return;
|
|
171
374
|
if (selected === previous) return;
|
|
172
375
|
state.selected = selected;
|
|
376
|
+
const nextWindow = pickerWindow(rows, selected, state.windowSize, state.offset);
|
|
377
|
+
if (nextWindow.offset !== previousOffset) {
|
|
378
|
+
renderPicker(state, { clear: false });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
173
381
|
renderRow(state, previous);
|
|
174
382
|
renderRow(state, selected);
|
|
175
383
|
};
|
|
@@ -222,6 +430,7 @@ export async function chooseLaunchContext(context) {
|
|
|
222
430
|
}
|
|
223
431
|
|
|
224
432
|
export async function newConversation(options) {
|
|
433
|
+
if (options.print) throw new Error('--print is only supported with sessions');
|
|
225
434
|
const context = createLaunchContext(options);
|
|
226
435
|
if (!context.model) {
|
|
227
436
|
print(missingModelMessage(context));
|
|
@@ -233,9 +442,5 @@ export async function newConversation(options) {
|
|
|
233
442
|
const selected = await chooseLaunchContext(context);
|
|
234
443
|
if (!selected) return;
|
|
235
444
|
}
|
|
236
|
-
if (options.print || (!hasLaunchOverrides && !canPick)) {
|
|
237
|
-
print(`${codexNewCommand(context)}\n`);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
445
|
await runCodex(codexNewArgs(context));
|
|
241
446
|
}
|
package/src/codex-sessions.js
CHANGED
|
@@ -27,7 +27,7 @@ const ID_WIDTH = 36;
|
|
|
27
27
|
const TITLE_WIDTH = 16;
|
|
28
28
|
const TABLE_INDENT = ' ';
|
|
29
29
|
const COLUMN_GAP = ' ';
|
|
30
|
-
const
|
|
30
|
+
export const DEFAULT_SESSION_LIMIT = 15;
|
|
31
31
|
|
|
32
32
|
function print(message = '') {
|
|
33
33
|
process.stdout.write(message);
|
|
@@ -186,7 +186,7 @@ function resolveSessionSelection(selection, sessionsList) {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
function printSessions(sessionsList, options, context) {
|
|
189
|
-
const listed = sessionsList.slice(0, options.limit ||
|
|
189
|
+
const listed = sessionsList.slice(0, options.limit || DEFAULT_SESSION_LIMIT);
|
|
190
190
|
print(`Codex sessions ${options.all ? `under ${context.codexHome}` : `for project ${context.projectRoot}`}\n`);
|
|
191
191
|
print(`Target: ${context.provider} / ${context.model} / ${context.reasoningEffort}\n\n`);
|
|
192
192
|
if (!listed.length) {
|
|
@@ -215,23 +215,35 @@ function sessionRows(sessionsList) {
|
|
|
215
215
|
return sessionsList.map((session) => sessionRow(session));
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
export function sessionPickerRows(allSessions) {
|
|
219
|
+
return sessionRows(allSessions);
|
|
220
|
+
}
|
|
221
|
+
|
|
218
222
|
async function chooseSessionFlow(allSessions, context, limit) {
|
|
219
223
|
if (!context.models.length) {
|
|
220
224
|
print(missingModelMessage(context));
|
|
221
225
|
return null;
|
|
222
226
|
}
|
|
223
227
|
|
|
224
|
-
const listed = allSessions.slice(0, limit);
|
|
225
228
|
const header = sessionHeader();
|
|
229
|
+
const windowSize = limit || DEFAULT_SESSION_LIMIT;
|
|
226
230
|
return await withPickerScreen(async () => {
|
|
227
231
|
let step = 'session';
|
|
228
232
|
while (true) {
|
|
229
233
|
if (step === 'session') {
|
|
230
|
-
const
|
|
231
|
-
|
|
234
|
+
const result = await pick(
|
|
235
|
+
`Choose Codex session`,
|
|
236
|
+
sessionPickerRows(allSessions),
|
|
237
|
+
header,
|
|
238
|
+
{
|
|
239
|
+
windowSize,
|
|
240
|
+
emptyMessage: 'No matching sessions found.',
|
|
241
|
+
shortcuts: [{ key: 'n', action: 'new', label: 'N new' }],
|
|
242
|
+
},
|
|
243
|
+
);
|
|
232
244
|
if (result.action === 'cancel') return null;
|
|
233
245
|
if (result.action === 'back') return null;
|
|
234
|
-
context.selectedSession = result.
|
|
246
|
+
context.selectedSession = result.action === 'new' ? { newConversation: true } : allSessions[result.index];
|
|
235
247
|
step = 'model';
|
|
236
248
|
} else if (step === 'model') {
|
|
237
249
|
const action = await pickModel(context);
|
|
@@ -273,7 +285,7 @@ export async function sessions(options) {
|
|
|
273
285
|
return;
|
|
274
286
|
}
|
|
275
287
|
|
|
276
|
-
const selected = await chooseSessionFlow(allSessions, context, options.limit ||
|
|
288
|
+
const selected = await chooseSessionFlow(allSessions, context, options.limit || DEFAULT_SESSION_LIMIT);
|
|
277
289
|
if (selected?.newConversation) await runCodex(codexNewArgs(context));
|
|
278
290
|
else if (selected) await runCodex(codexResumeArgs(selected.id, context));
|
|
279
291
|
}
|
package/src/config.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { loadModelAliases } from './model-map.js';
|
|
2
|
-
import { mergeLocalConfig } from './local-config.js';
|
|
2
|
+
import { mergeLocalConfig, readLocalConfigFile, resolveLocalConfigPath } from './local-config.js';
|
|
3
3
|
import { parseBoolean, parseList } from './common.js';
|
|
4
4
|
import { readCodexConfig } from './codex-config.js';
|
|
5
|
+
import { normalizePromptLanguage } from './prompt-language.js';
|
|
5
6
|
|
|
6
7
|
export function loadConfig(env = process.env) {
|
|
7
8
|
const mergedEnv = mergeLocalConfig(env);
|
|
9
|
+
const localConfig = readLocalConfigFile(resolveLocalConfigPath(env));
|
|
8
10
|
const codexConfig = readCodexConfig(mergedEnv);
|
|
9
11
|
return {
|
|
10
12
|
port: Number(mergedEnv.PORT || 3000),
|
|
@@ -54,6 +56,7 @@ export function loadConfig(env = process.env) {
|
|
|
54
56
|
codexReasoningSummary: codexConfig.modelReasoningSummary,
|
|
55
57
|
codexModelSupportsReasoningSummaries: codexConfig.modelSupportsReasoningSummaries,
|
|
56
58
|
codexHideAgentReasoning: codexConfig.hideAgentReasoning,
|
|
59
|
+
codexPromptLanguage: normalizePromptLanguage(localConfig.CODEX_PROMPT_LANGUAGE),
|
|
57
60
|
modelAliases: loadModelAliases(mergedEnv),
|
|
58
61
|
};
|
|
59
62
|
}
|
package/src/local-config.js
CHANGED
|
@@ -36,10 +36,14 @@ export function readLocalConfigFile(path = resolve(PROJECT_ROOT, 'config', 'gate
|
|
|
36
36
|
return flattenConfig(parsed.value);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function
|
|
40
|
-
|
|
39
|
+
export function resolveLocalConfigPath(env = process.env, cwd = PROJECT_ROOT) {
|
|
40
|
+
return env.GATEWAY_CONFIG_FILE
|
|
41
41
|
? resolve(cwd, env.GATEWAY_CONFIG_FILE)
|
|
42
42
|
: resolve(cwd, 'config', 'gateway.local.json');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function mergeLocalConfig(env = process.env, cwd = PROJECT_ROOT) {
|
|
46
|
+
const configPath = resolveLocalConfigPath(env, cwd);
|
|
43
47
|
const fileConfig = readLocalConfigFile(configPath);
|
|
44
48
|
if (fileConfig.UPSTREAM_API_KEY === 'sk-REPLACE_ME') {
|
|
45
49
|
delete fileConfig.UPSTREAM_API_KEY;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const DEFAULT_PROMPT_LANGUAGE = 'en';
|
|
2
|
+
export const PROMPT_LANGUAGES = ['en', 'zh'];
|
|
3
|
+
|
|
4
|
+
export function normalizePromptLanguage(value) {
|
|
5
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
6
|
+
return PROMPT_LANGUAGES.includes(normalized) ? normalized : DEFAULT_PROMPT_LANGUAGE;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function catalogFileForPromptLanguage(language) {
|
|
10
|
+
return normalizePromptLanguage(language) === 'zh'
|
|
11
|
+
? 'codex-model-catalog.zh.json'
|
|
12
|
+
: 'codex-model-catalog.json';
|
|
13
|
+
}
|