@galaxy-yearn/codex-deepseek-gateway 0.1.5 → 0.1.6

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.
@@ -1,13 +1,23 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { existsSync, readFileSync } from 'node:fs';
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';
6
7
 
7
8
  export const DEFAULT_PROVIDER = 'deepseek-gateway';
8
9
  export const REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
9
- const SELECTED_ROW = '\x1b[38;5;81m';
10
+ const SELECTED_ROW = '\x1b[38;2;91;124;255m';
10
11
  const RESET_STYLE = '\x1b[0m';
12
+ const require = createRequire(import.meta.url);
13
+ const CODEX_PLATFORM_PACKAGE_BY_TARGET = {
14
+ 'x86_64-unknown-linux-musl': '@openai/codex-linux-x64',
15
+ 'aarch64-unknown-linux-musl': '@openai/codex-linux-arm64',
16
+ 'x86_64-apple-darwin': '@openai/codex-darwin-x64',
17
+ 'aarch64-apple-darwin': '@openai/codex-darwin-arm64',
18
+ 'x86_64-pc-windows-msvc': '@openai/codex-win32-x64',
19
+ 'aarch64-pc-windows-msvc': '@openai/codex-win32-arm64',
20
+ };
11
21
 
12
22
  function print(message = '') {
13
23
  process.stdout.write(message);
@@ -46,6 +56,7 @@ export function createLaunchContext(options) {
46
56
  all: options.all,
47
57
  codexHome,
48
58
  installDir: options.dir,
59
+ modelCatalogPath: join(options.dir, 'config', 'codex-model-catalog.json'),
49
60
  models,
50
61
  projectRoot: findProjectRoot(process.cwd()),
51
62
  provider: options.provider || DEFAULT_PROVIDER,
@@ -59,7 +70,7 @@ export function missingModelMessage(context) {
59
70
  }
60
71
 
61
72
  function configOverrideArgs(context) {
62
- return [
73
+ const args = [
63
74
  '-c',
64
75
  `model_provider=${JSON.stringify(context.provider)}`,
65
76
  '-c',
@@ -71,21 +82,10 @@ function configOverrideArgs(context) {
71
82
  '-c',
72
83
  'model_reasoning_summary="auto"',
73
84
  ];
74
- }
75
-
76
- function configOverrideCommandParts(context) {
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
- ];
85
+ if (context.provider === DEFAULT_PROVIDER && context.modelCatalogPath) {
86
+ args.push('-c', `model_catalog_json=${JSON.stringify(context.modelCatalogPath)}`);
87
+ }
88
+ return args;
89
89
  }
90
90
 
91
91
  export function codexNewArgs(context) {
@@ -96,20 +96,146 @@ export function codexResumeArgs(sessionId, context) {
96
96
  return ['resume', sessionId, ...configOverrideArgs(context)];
97
97
  }
98
98
 
99
- export function codexNewCommand(context) {
100
- return ['codex', ...configOverrideCommandParts(context)].join(' ');
99
+ function quoteShellArg(arg) {
100
+ const value = String(arg);
101
+ if (/^[A-Za-z0-9_./:=@%+,-]+$/.test(value)) return value;
102
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
101
103
  }
102
104
 
103
105
  export function codexResumeCommand(sessionId, context) {
104
- return ['codex', 'resume', sessionId, ...configOverrideCommandParts(context)].join(' ');
106
+ return ['codex', ...codexResumeArgs(sessionId, context)].map(quoteShellArg).join(' ');
107
+ }
108
+
109
+ function codexTargetTriple(platform = process.platform, arch = process.arch) {
110
+ if ((platform === 'linux' || platform === 'android') && arch === 'x64') return 'x86_64-unknown-linux-musl';
111
+ if ((platform === 'linux' || platform === 'android') && arch === 'arm64') return 'aarch64-unknown-linux-musl';
112
+ if (platform === 'darwin' && arch === 'x64') return 'x86_64-apple-darwin';
113
+ if (platform === 'darwin' && arch === 'arm64') return 'aarch64-apple-darwin';
114
+ if (platform === 'win32' && arch === 'x64') return 'x86_64-pc-windows-msvc';
115
+ if (platform === 'win32' && arch === 'arm64') return 'aarch64-pc-windows-msvc';
116
+ return '';
117
+ }
118
+
119
+ function pathEntries(env = process.env) {
120
+ const pathValue = env.PATH || env.Path || env.path || '';
121
+ return pathValue.split(process.platform === 'win32' ? ';' : ':').filter(Boolean);
122
+ }
123
+
124
+ function candidateExecutables(command, env = process.env) {
125
+ if (command.includes('/') || command.includes('\\')) return [command];
126
+ const extensions = process.platform === 'win32'
127
+ ? (env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
128
+ : [''];
129
+ const names = process.platform === 'win32'
130
+ ? extensions.map((extension) => `${command}${extension.toLowerCase()}`)
131
+ : [command];
132
+ return pathEntries(env).flatMap((entry) => names.map((name) => join(entry, name)));
133
+ }
134
+
135
+ function npmShimCodexPackageRoot(shimPath) {
136
+ const base = dirname(shimPath);
137
+ const packageRoot = join(base, 'node_modules', '@openai', 'codex');
138
+ return existsSync(join(packageRoot, 'package.json')) ? packageRoot : '';
139
+ }
140
+
141
+ function ancestorCodexPackageRoot(filePath) {
142
+ try {
143
+ let current = dirname(realpathSync(filePath));
144
+ while (true) {
145
+ const packageJson = join(current, 'package.json');
146
+ if (existsSync(packageJson)) {
147
+ try {
148
+ const parsed = JSON.parse(readFileSync(packageJson, 'utf8'));
149
+ if (parsed?.name === '@openai/codex') return current;
150
+ } catch {
151
+ // Keep walking; this is only a best-effort executable locator.
152
+ }
153
+ }
154
+ const parent = dirname(current);
155
+ if (parent === current) return '';
156
+ current = parent;
157
+ }
158
+ } catch {
159
+ return '';
160
+ }
161
+ }
162
+
163
+ function codexPackageRoot() {
164
+ try {
165
+ return dirname(require.resolve('@openai/codex/package.json'));
166
+ } catch {
167
+ for (const candidate of candidateExecutables('codex')) {
168
+ const packageRoot = npmShimCodexPackageRoot(candidate) || ancestorCodexPackageRoot(candidate);
169
+ if (packageRoot) return packageRoot;
170
+ }
171
+ return '';
172
+ }
173
+ }
174
+
175
+ function codexPlatformPackageJson(packageRoot, platformPackage) {
176
+ try {
177
+ return createRequire(join(packageRoot, 'package.json')).resolve(`${platformPackage}/package.json`);
178
+ } catch {
179
+ const nested = join(packageRoot, 'node_modules', platformPackage, 'package.json');
180
+ return existsSync(nested) ? nested : '';
181
+ }
182
+ }
183
+
184
+ export function resolveCodexExecutable() {
185
+ const packageRoot = codexPackageRoot();
186
+ const targetTriple = codexTargetTriple();
187
+ const platformPackage = CODEX_PLATFORM_PACKAGE_BY_TARGET[targetTriple];
188
+ if (packageRoot && targetTriple && platformPackage) {
189
+ const packageJson = codexPlatformPackageJson(packageRoot, platformPackage);
190
+ if (existsSync(packageJson)) {
191
+ const executable = join(
192
+ dirname(packageJson),
193
+ 'vendor',
194
+ targetTriple,
195
+ 'bin',
196
+ process.platform === 'win32' ? 'codex.exe' : 'codex',
197
+ );
198
+ if (existsSync(executable)) return executable;
199
+ }
200
+ const bundledExecutable = join(
201
+ packageRoot,
202
+ 'vendor',
203
+ targetTriple,
204
+ 'bin',
205
+ process.platform === 'win32' ? 'codex.exe' : 'codex',
206
+ );
207
+ if (existsSync(bundledExecutable)) return bundledExecutable;
208
+ }
209
+
210
+ for (const candidate of candidateExecutables('codex')) {
211
+ if (existsSync(candidate) && !candidate.endsWith('.cmd') && !candidate.endsWith('.bat') && !candidate.endsWith('.ps1')) {
212
+ return candidate;
213
+ }
214
+ }
215
+ return 'codex';
216
+ }
217
+
218
+ export function codexProcessEnv() {
219
+ const packageRoot = codexPackageRoot();
220
+ if (!packageRoot) return process.env;
221
+ return {
222
+ ...process.env,
223
+ CODEX_MANAGED_BY_NPM: '1',
224
+ CODEX_MANAGED_PACKAGE_ROOT: realpathSync(packageRoot),
225
+ };
105
226
  }
106
227
 
107
228
  export async function runCodex(args) {
108
- const child = spawn('codex', args, {
229
+ const child = spawn(resolveCodexExecutable(), args, {
109
230
  stdio: 'inherit',
110
- shell: process.platform === 'win32',
231
+ shell: false,
232
+ env: codexProcessEnv(),
111
233
  });
112
234
  process.exitCode = await new Promise((resolveChild) => {
235
+ child.on('error', (error) => {
236
+ process.stderr.write(`Failed to launch codex: ${error.message}\n`);
237
+ resolveChild(1);
238
+ });
113
239
  child.on('exit', (exitCode) => resolveChild(exitCode ?? 0));
114
240
  });
115
241
  }
@@ -118,25 +244,73 @@ function rowOffset(header) {
118
244
  return header ? 4 : 2;
119
245
  }
120
246
 
121
- function renderRow(state, index) {
122
- cursorTo(process.stdout, 0, rowOffset(state.header) + index);
123
- const row = `${index === state.selected ? '>' : ' '} ${state.rows[index]}`;
124
- const styled = index === state.selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row;
247
+ export function pickerWindow(rows, selected, windowSize, currentOffset = 0) {
248
+ const allRows = Array.isArray(rows) ? rows : [];
249
+ const size = Math.max(1, Math.trunc(Number(windowSize) || allRows.length || 1));
250
+ const clampedSelected = Math.min(Math.max(0, selected), Math.max(0, allRows.length - 1));
251
+ const maxOffset = Math.max(0, allRows.length - size);
252
+ let offset = Math.min(Math.max(0, Math.trunc(Number(currentOffset) || 0)), maxOffset);
253
+ if (clampedSelected < offset) {
254
+ offset = clampedSelected;
255
+ } else if (clampedSelected >= offset + size) {
256
+ offset = clampedSelected - size + 1;
257
+ }
258
+ return {
259
+ offset,
260
+ rows: allRows.slice(offset, offset + size),
261
+ };
262
+ }
263
+
264
+ function pickerControls(shortcuts = []) {
265
+ const labels = shortcuts.map((shortcut) => shortcut.label).filter(Boolean);
266
+ return [...labels, '↑/↓ select', '← back', 'Enter confirm', 'Esc quit'].join(' ');
267
+ }
268
+
269
+ function renderRow(state, absoluteIndex) {
270
+ if (absoluteIndex < state.offset || absoluteIndex >= state.offset + state.visibleRows.length) return;
271
+ const visibleIndex = absoluteIndex - state.offset;
272
+ cursorTo(process.stdout, 0, rowOffset(state.header) + visibleIndex);
273
+ const row = `${absoluteIndex === state.selected ? '>' : ' '} ${state.rows[absoluteIndex]}`;
274
+ const styled = absoluteIndex === state.selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row;
125
275
  print(`\x1b[2K${styled}`);
126
276
  }
127
277
 
128
- function renderPicker(state) {
129
- const { title, rows, selected, header } = state;
278
+ function pickerLines(state) {
279
+ const { title, rows, selected, header, emptyMessage, shortcuts } = state;
280
+ const window = pickerWindow(rows, selected, state.windowSize, state.offset);
281
+ state.offset = window.offset;
282
+ state.visibleRows = window.rows;
283
+ const lines = [title, ''];
284
+ if (header) lines.push(` ${header}`, ` ${'-'.repeat(header.length)}`);
285
+ for (const [visibleIndex, rowText] of state.visibleRows.entries()) {
286
+ const absoluteIndex = state.offset + visibleIndex;
287
+ const row = `${absoluteIndex === selected ? '>' : ' '} ${rowText}`;
288
+ lines.push(absoluteIndex === selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row);
289
+ }
290
+ if (!rows.length && emptyMessage) lines.push(emptyMessage);
291
+ if (rows.length > state.visibleRows.length) {
292
+ lines.push('', ` Showing ${state.offset + 1}-${state.offset + state.visibleRows.length} of ${rows.length}`);
293
+ }
294
+ lines.push('', ` ${pickerControls(shortcuts)}`);
295
+ return lines;
296
+ }
297
+
298
+ function renderPicker(state, { clear = true } = {}) {
299
+ const lines = pickerLines(state);
130
300
  cursorTo(process.stdout, 0, 0);
131
- clearScreenDown(process.stdout);
132
- let output = `${title}\n\n`;
133
- if (header) output += ` ${header}\n ${'-'.repeat(header.length)}\n`;
134
- for (const [index] of rows.entries()) {
135
- const row = `${index === selected ? '>' : ' '} ${rows[index]}`;
136
- output += `${index === selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row}\n`;
301
+ if (clear) {
302
+ clearScreenDown(process.stdout);
303
+ print(`${lines.join('\n')}\n`);
304
+ state.renderedLineCount = lines.length;
305
+ return;
306
+ }
307
+ for (const line of lines) {
308
+ print(`\x1b[2K${line}\n`);
137
309
  }
138
- output += '\nUp/Down select Enter confirm Left back Esc quit\n';
139
- print(output);
310
+ for (let index = lines.length; index < (state.renderedLineCount || 0); index += 1) {
311
+ print('\x1b[2K\n');
312
+ }
313
+ state.renderedLineCount = lines.length;
140
314
  }
141
315
 
142
316
  function openPickerScreen() {
@@ -147,11 +321,23 @@ function closePickerScreen() {
147
321
  process.stdout.write('\x1b[?25h\x1b[?1049l');
148
322
  }
149
323
 
150
- export async function pick(title, rows, header = '') {
151
- if (!rows.length) return { action: 'back' };
324
+ export async function pick(title, rows, header = '', options = {}) {
325
+ const shortcuts = Array.isArray(options.shortcuts) ? options.shortcuts : [];
326
+ if (!rows.length && !shortcuts.length) return { action: 'back' };
152
327
  let selected = 0;
153
328
  const stdin = process.stdin;
154
- const state = { title, rows, selected, header };
329
+ const state = {
330
+ title,
331
+ rows,
332
+ selected,
333
+ header,
334
+ shortcuts,
335
+ windowSize: options.windowSize,
336
+ emptyMessage: options.emptyMessage,
337
+ offset: 0,
338
+ visibleRows: [],
339
+ renderedLineCount: 0,
340
+ };
155
341
  renderPicker(state);
156
342
 
157
343
  return await new Promise((resolvePick) => {
@@ -163,13 +349,25 @@ export async function pick(title, rows, header = '') {
163
349
  const key = chunk.toString('utf8');
164
350
  if (key === '\u0003' || key === '\u001b') return done({ action: 'cancel' });
165
351
  if (key === '\u001b[D') return done({ action: 'back' });
166
- if (key === '\r' || key === '\n') return done({ action: 'select', index: selected });
352
+ for (const shortcut of shortcuts) {
353
+ if (key.toLowerCase() === String(shortcut.key || '').toLowerCase()) {
354
+ return done({ action: shortcut.action || shortcut.key });
355
+ }
356
+ }
357
+ if ((key === '\r' || key === '\n') && rows.length) return done({ action: 'select', index: selected });
358
+ if (!rows.length) return;
167
359
  const previous = selected;
360
+ const previousOffset = state.offset;
168
361
  if (key === '\u001b[A') selected = Math.max(0, selected - 1);
169
362
  else if (key === '\u001b[B') selected = Math.min(rows.length - 1, selected + 1);
170
363
  else return;
171
364
  if (selected === previous) return;
172
365
  state.selected = selected;
366
+ const nextWindow = pickerWindow(rows, selected, state.windowSize, state.offset);
367
+ if (nextWindow.offset !== previousOffset) {
368
+ renderPicker(state, { clear: false });
369
+ return;
370
+ }
173
371
  renderRow(state, previous);
174
372
  renderRow(state, selected);
175
373
  };
@@ -222,6 +420,7 @@ export async function chooseLaunchContext(context) {
222
420
  }
223
421
 
224
422
  export async function newConversation(options) {
423
+ if (options.print) throw new Error('--print is only supported with sessions');
225
424
  const context = createLaunchContext(options);
226
425
  if (!context.model) {
227
426
  print(missingModelMessage(context));
@@ -233,9 +432,5 @@ export async function newConversation(options) {
233
432
  const selected = await chooseLaunchContext(context);
234
433
  if (!selected) return;
235
434
  }
236
- if (options.print || (!hasLaunchOverrides && !canPick)) {
237
- print(`${codexNewCommand(context)}\n`);
238
- return;
239
- }
240
435
  await runCodex(codexNewArgs(context));
241
436
  }
@@ -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 NEW_SESSION_ROW = '[New conversation]';
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 || 20);
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 rows = [NEW_SESSION_ROW, ...sessionRows(listed)];
231
- const result = await pick(`Choose Codex session`, rows, header);
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.index === 0 ? { newConversation: true } : listed[result.index - 1];
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 || 20);
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
  }