@galaxy-yearn/codex-deepseek-gateway 0.1.3 → 0.1.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Codex DeepSeek Gateway
2
2
 
3
- A small local gateway for using DeepSeek V4 models from Codex.
3
+ A small local gateway for using DeepSeek models from Codex.
4
4
 
5
5
  Codex keeps sending OpenAI `Responses API` requests to a local endpoint. The gateway converts those requests to DeepSeek-compatible `Chat Completions`, calls DeepSeek, then converts the result back to Responses JSON or streaming `response.*` events.
6
6
 
@@ -43,7 +43,7 @@ Put your DeepSeek API key in `gateway.local.json`:
43
43
  "upstreamApiKey": "sk-..."
44
44
  ```
45
45
 
46
- `model-aliases.json` controls the gateway-facing model IDs exposed on `GET /v1/models` and used by the `sessions` picker. Edit it only if you want to add or rename model aliases.
46
+ `model-aliases.json` controls the gateway-facing model IDs exposed on `GET /v1/models` and used by the `new` and `sessions` pickers. Edit it only if you want to add or rename model aliases.
47
47
 
48
48
  If the key is already configured, `install` also starts the gateway. If this is your first install, add the key and then run:
49
49
 
@@ -97,7 +97,7 @@ Important fields:
97
97
  - `codexReasoningEffort` should match `model_reasoning_effort`
98
98
  - `deepseekThinking` shows the DeepSeek `thinking` payload
99
99
  - `deepseekReasoningEffort` shows the DeepSeek effort sent upstream
100
- - `gatewayEmitsReasoningSummary` should be `true` when DeepSeek thinking is enabled
100
+ - `reasoningDisplayMode` shows whether reasoning is shown as `summary`, `hidden`, or `disabled`
101
101
  - `tavilyWebSearchReady` and `firecrawlWebFetchReady` show whether optional web search backends are usable
102
102
 
103
103
  Avoid running Codex through a proxy while using the local gateway. Some proxy clients intercept `http://127.0.0.1:3000` and can break local gateway requests.
@@ -125,7 +125,7 @@ The gateway serves these aliases directly on `GET /v1/models`. If you also want
125
125
  "fetchUpstreamModels": true
126
126
  ```
127
127
 
128
- Whether Codex TUI `/model` shows custom provider models depends on the Codex client build. `config.toml` and the `sessions` command remain the reliable ways to choose a model.
128
+ Whether Codex TUI `/model` shows custom provider models depends on the Codex client build. `config.toml`, `new`, and `sessions` remain the reliable ways to choose a model.
129
129
 
130
130
  ## Reasoning
131
131
 
@@ -171,6 +171,34 @@ DeepSeek receives compact text context containing the search query, sources, sni
171
171
 
172
172
  The web payload is intentionally text-focused. Tavily is called with `include_raw_content: false`; Firecrawl defaults to main content, removes base64 images, rejects local/private URLs, and truncates page text before it reaches the model.
173
173
 
174
+ ## New Conversations
175
+
176
+ Start a new Codex conversation with a temporary gateway model override:
177
+
178
+ ```sh
179
+ npx @galaxy-yearn/codex-deepseek-gateway new
180
+ ```
181
+
182
+ The command first lets you choose a model from `~/.codex/deepseek-gateway/config/model-aliases.json`, then choose Codex reasoning effort, then runs:
183
+
184
+ ```sh
185
+ codex -c model_provider=deepseek-gateway -c model=<model> -c model_reasoning_effort=<effort>
186
+ ```
187
+
188
+ This does not edit `~/.codex/config.toml`; the `model_providers.deepseek-gateway` provider entry still needs to exist there.
189
+
190
+ You can skip the picker:
191
+
192
+ ```sh
193
+ npx @galaxy-yearn/codex-deepseek-gateway new --model deepseek-v4-flash --reasoning-effort low
194
+ ```
195
+
196
+ Print the generated launch command instead of running Codex:
197
+
198
+ ```sh
199
+ npx @galaxy-yearn/codex-deepseek-gateway new --print
200
+ ```
201
+
174
202
  ## Sessions
175
203
 
176
204
  Open a cross-provider session picker from a project:
@@ -189,8 +217,8 @@ Flow:
189
217
 
190
218
  - choose a model from `~/.codex/deepseek-gateway/config/model-aliases.json`
191
219
  - choose Codex reasoning effort
192
- - choose the session to resume
193
- - use `Up/Down` to select, `Enter` to confirm, `Left` to go back, and `Esc` to quit
220
+ - choose `[New conversation]` or the session to resume
221
+ - use `↑/↓` to select, `Enter` to confirm, `←` to go back, and `Esc` to quit
194
222
 
195
223
  Print copyable resume commands instead of opening the picker:
196
224
 
@@ -204,6 +232,18 @@ Include sessions outside the current project:
204
232
  npx @galaxy-yearn/codex-deepseek-gateway sessions --all
205
233
  ```
206
234
 
235
+ Resume a matching session directly by row number or unique session id prefix:
236
+
237
+ ```sh
238
+ npx @galaxy-yearn/codex-deepseek-gateway sessions --exec <id-or-row>
239
+ ```
240
+
241
+ Limit how many session rows are shown or offered in the picker:
242
+
243
+ ```sh
244
+ npx @galaxy-yearn/codex-deepseek-gateway sessions --limit 50
245
+ ```
246
+
207
247
  The command does not edit session files, change provider ownership, or change Codex's native resume picker filters. It only helps you find the hidden session id and resume it with explicit config overrides.
208
248
 
209
249
  ## Commands
@@ -214,6 +254,7 @@ npx @galaxy-yearn/codex-deepseek-gateway start
214
254
  npx @galaxy-yearn/codex-deepseek-gateway stop
215
255
  npx @galaxy-yearn/codex-deepseek-gateway status
216
256
  npx @galaxy-yearn/codex-deepseek-gateway doctor
257
+ npx @galaxy-yearn/codex-deepseek-gateway new
217
258
  npx @galaxy-yearn/codex-deepseek-gateway sessions
218
259
  npx @galaxy-yearn/codex-deepseek-gateway uninstall
219
260
  ```
@@ -233,6 +274,7 @@ If `start` returns without visible output on your terminal, run `status`; `"reac
233
274
  - lightweight local `previous_response_id` / `conversation` history while the gateway process is running
234
275
  - `GET /v1/models` with local model aliases and optional upstream discovery
235
276
  - optional Tavily/Firecrawl-backed `web_search` emulation
277
+ - new-conversation launcher with per-run Codex config overrides
236
278
  - read-only cross-provider session picker
237
279
 
238
280
  ## Limits
@@ -244,7 +286,7 @@ Chat Completions is not a full Responses API replacement. Some Responses feature
244
286
  - URL citations are returned in the Responses metadata path. Whether they appear as clickable links in the terminal depends on the Codex client build and how it renders custom-provider citation annotations.
245
287
  - OpenAI `file_id` values are passed through; the gateway cannot fetch private OpenAI-hosted files.
246
288
  - In-memory conversation history is lost when the gateway restarts.
247
- - The gateway exposes model aliases on `/v1/models`, including aliases from `config/model-aliases.json`. Whether Codex TUI `/model` actually shows custom provider models depends on the Codex build. `config.toml` remains the reliable fallback.
289
+ - The gateway exposes model aliases on `/v1/models`, including aliases from `config/model-aliases.json`. Whether Codex TUI `/model` actually shows custom provider models depends on the Codex build. `config.toml`, `new`, and `sessions` remain the reliable ways to choose a model.
248
290
 
249
291
  ## License
250
292
 
@@ -13,6 +13,7 @@ import { createRequire } from 'node:module';
13
13
  import { dirname, join, resolve } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { loadConfig } from '../src/config.js';
16
+ import { newConversation } from '../src/codex-launch.js';
16
17
  import { sessions } from '../src/codex-sessions.js';
17
18
  import { toProviderChatCompletionsRequest } from '../src/protocol.js';
18
19
 
@@ -33,6 +34,7 @@ Usage:
33
34
  codex-deepseek-gateway stop
34
35
  codex-deepseek-gateway status
35
36
  codex-deepseek-gateway doctor
37
+ codex-deepseek-gateway new
36
38
  codex-deepseek-gateway sessions
37
39
  codex-deepseek-gateway uninstall
38
40
 
@@ -40,13 +42,14 @@ Options:
40
42
  --dir <path> Install directory, defaults to ~/.codex/deepseek-gateway
41
43
  --no-edit Do not open the local config file after install
42
44
  --all With sessions, include sessions outside the current project
43
- --provider <id> With sessions, target model_provider override
44
- --model <id> With sessions, target model override
45
+ --provider <id> With new/sessions, target model_provider override
46
+ --model <id> With new/sessions, target model override
45
47
  --reasoning-effort <level>
46
- With sessions, target Codex reasoning effort
48
+ With new/sessions, target Codex reasoning effort
47
49
  --exec <id> With sessions, run the generated codex resume command
48
50
  --limit <n> With sessions, max rows to print, defaults to 20
49
- --print With sessions, print copyable resume commands instead of picker
51
+ --print With new, print the launch command. With sessions, print
52
+ copyable resume commands instead of picker
50
53
  `);
51
54
  }
52
55
 
@@ -363,7 +366,6 @@ async function doctor(options) {
363
366
  : thinkingEnabled
364
367
  ? 'summary'
365
368
  : 'disabled';
366
- const codexSummaryConfigured = supportsReasoningSummaries && summaryMode && summaryMode !== 'none';
367
369
  print(JSON.stringify({
368
370
  packageVersion: packageJson.version,
369
371
  installDir: options.dir,
@@ -380,8 +382,6 @@ async function doctor(options) {
380
382
  deepseekThinking: upstreamRequest.thinking || null,
381
383
  deepseekReasoningEffort: upstreamRequest.reasoning_effort || null,
382
384
  reasoningDisplayMode,
383
- gatewayEmitsReasoningSummary: reasoningDisplayMode === 'summary',
384
- codexSummaryConfigured,
385
385
  tavilyWebSearchEnabled: Boolean(config.tavilyWebSearchEnabled),
386
386
  tavilyWebSearchReady: Boolean(config.tavilyWebSearchEnabled && config.tavilyApiKey),
387
387
  firecrawlWebFetchEnabled: Boolean(config.firecrawlWebFetchEnabled),
@@ -404,7 +404,7 @@ async function main() {
404
404
  usage();
405
405
  return;
406
406
  }
407
- const commands = { install, start, stop, status, doctor, sessions, uninstall };
407
+ const commands = { install, start, stop, status, doctor, new: newConversation, sessions, uninstall };
408
408
  const handler = commands[command];
409
409
  if (!handler) {
410
410
  throw new Error(`Unknown command: ${command}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galaxy-yearn/codex-deepseek-gateway",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Local OpenAI Codex Responses API gateway for DeepSeek Chat Completions.",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -0,0 +1,233 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { clearScreenDown, cursorTo } from 'node:readline';
5
+ import { readCodexConfig } from './codex-config.js';
6
+
7
+ export const DEFAULT_PROVIDER = 'deepseek-gateway';
8
+ export const REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
9
+ const SELECTED_ROW = '\x1b[38;5;81m';
10
+ const RESET_STYLE = '\x1b[0m';
11
+
12
+ function print(message = '') {
13
+ process.stdout.write(message);
14
+ }
15
+
16
+ export function defaultCodexHome() {
17
+ return process.env.CODEX_HOME || join(process.env.USERPROFILE || process.env.HOME || process.cwd(), '.codex');
18
+ }
19
+
20
+ export function findProjectRoot(start) {
21
+ let current = resolve(start);
22
+ while (true) {
23
+ if (existsSync(join(current, '.git'))) return current;
24
+ const parent = dirname(current);
25
+ if (parent === current) return resolve(start);
26
+ current = parent;
27
+ }
28
+ }
29
+
30
+ function readJsonObject(file) {
31
+ if (!existsSync(file)) return {};
32
+ const value = JSON.parse(readFileSync(file, 'utf8'));
33
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
34
+ }
35
+
36
+ export function gatewayModels(installDir) {
37
+ return Object.keys(readJsonObject(join(installDir, 'config', 'model-aliases.json'))).sort();
38
+ }
39
+
40
+ export function createLaunchContext(options) {
41
+ const codexHome = defaultCodexHome();
42
+ const models = gatewayModels(options.dir);
43
+ const codexConfig = readCodexConfig();
44
+ const configModel = codexConfig.modelProvider === DEFAULT_PROVIDER && models.includes(codexConfig.model) ? codexConfig.model : '';
45
+ return {
46
+ all: options.all,
47
+ codexHome,
48
+ installDir: options.dir,
49
+ models,
50
+ projectRoot: findProjectRoot(process.cwd()),
51
+ provider: options.provider || DEFAULT_PROVIDER,
52
+ model: options.model || configModel || models[0] || '',
53
+ reasoningEffort: options.reasoningEffort || codexConfig.modelReasoningEffort || 'low',
54
+ };
55
+ }
56
+
57
+ export function missingModelMessage(context) {
58
+ return `No gateway models found in ${join(context.installDir, 'config', 'model-aliases.json')}\n`;
59
+ }
60
+
61
+ function configOverrideArgs(context) {
62
+ return [
63
+ '-c',
64
+ `model_provider=${JSON.stringify(context.provider)}`,
65
+ '-c',
66
+ `model=${JSON.stringify(context.model)}`,
67
+ '-c',
68
+ `model_reasoning_effort=${JSON.stringify(context.reasoningEffort)}`,
69
+ ];
70
+ }
71
+
72
+ function configOverrideCommandParts(context) {
73
+ return [
74
+ '-c',
75
+ `model_provider=${context.provider}`,
76
+ '-c',
77
+ `model=${context.model}`,
78
+ '-c',
79
+ `model_reasoning_effort=${context.reasoningEffort}`,
80
+ ];
81
+ }
82
+
83
+ export function codexNewArgs(context) {
84
+ return configOverrideArgs(context);
85
+ }
86
+
87
+ export function codexResumeArgs(sessionId, context) {
88
+ return ['resume', sessionId, ...configOverrideArgs(context)];
89
+ }
90
+
91
+ export function codexNewCommand(context) {
92
+ return ['codex', ...configOverrideCommandParts(context)].join(' ');
93
+ }
94
+
95
+ export function codexResumeCommand(sessionId, context) {
96
+ return ['codex', 'resume', sessionId, ...configOverrideCommandParts(context)].join(' ');
97
+ }
98
+
99
+ export async function runCodex(args) {
100
+ const child = spawn('codex', args, {
101
+ stdio: 'inherit',
102
+ shell: process.platform === 'win32',
103
+ });
104
+ process.exitCode = await new Promise((resolveChild) => {
105
+ child.on('exit', (exitCode) => resolveChild(exitCode ?? 0));
106
+ });
107
+ }
108
+
109
+ function rowOffset(header) {
110
+ return header ? 4 : 2;
111
+ }
112
+
113
+ function renderRow(state, index) {
114
+ cursorTo(process.stdout, 0, rowOffset(state.header) + index);
115
+ const row = `${index === state.selected ? '>' : ' '} ${state.rows[index]}`;
116
+ const styled = index === state.selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row;
117
+ print(`\x1b[2K${styled}`);
118
+ }
119
+
120
+ function renderPicker(state) {
121
+ const { title, rows, selected, header } = state;
122
+ cursorTo(process.stdout, 0, 0);
123
+ clearScreenDown(process.stdout);
124
+ let output = `${title}\n\n`;
125
+ if (header) output += ` ${header}\n ${'-'.repeat(header.length)}\n`;
126
+ for (const [index] of rows.entries()) {
127
+ const row = `${index === selected ? '>' : ' '} ${rows[index]}`;
128
+ output += `${index === selected ? `${SELECTED_ROW}${row}${RESET_STYLE}` : row}\n`;
129
+ }
130
+ output += '\n↑/↓ select Enter confirm ← back Esc quit\n';
131
+ print(output);
132
+ }
133
+
134
+ function openPickerScreen() {
135
+ process.stdout.write('\x1b[?1049h\x1b[?25l');
136
+ }
137
+
138
+ function closePickerScreen() {
139
+ process.stdout.write('\x1b[?25h\x1b[?1049l');
140
+ }
141
+
142
+ export async function pick(title, rows, header = '') {
143
+ if (!rows.length) return { action: 'back' };
144
+ let selected = 0;
145
+ const stdin = process.stdin;
146
+ const state = { title, rows, selected, header };
147
+ renderPicker(state);
148
+
149
+ return await new Promise((resolvePick) => {
150
+ const done = (result) => {
151
+ stdin.off('data', onData);
152
+ resolvePick(result);
153
+ };
154
+ const onData = (chunk) => {
155
+ const key = chunk.toString('utf8');
156
+ if (key === '\u0003' || key === '\u001b') return done({ action: 'cancel' });
157
+ if (key === '\u001b[D') return done({ action: 'back' });
158
+ if (key === '\r' || key === '\n') return done({ action: 'select', index: selected });
159
+ const previous = selected;
160
+ if (key === '\u001b[A') selected = Math.max(0, selected - 1);
161
+ else if (key === '\u001b[B') selected = Math.min(rows.length - 1, selected + 1);
162
+ else return;
163
+ if (selected === previous) return;
164
+ state.selected = selected;
165
+ renderRow(state, previous);
166
+ renderRow(state, selected);
167
+ };
168
+ stdin.on('data', onData);
169
+ });
170
+ }
171
+
172
+ export async function withPickerScreen(callback) {
173
+ const stdin = process.stdin;
174
+ stdin.resume();
175
+ stdin.setRawMode(true);
176
+ openPickerScreen();
177
+ try {
178
+ return await callback();
179
+ } finally {
180
+ stdin.setRawMode(false);
181
+ stdin.pause();
182
+ closePickerScreen();
183
+ }
184
+ }
185
+
186
+ export async function pickModel(context) {
187
+ const result = await pick('Choose gateway model', context.models);
188
+ if (result.action === 'select') context.model = context.models[result.index];
189
+ return result.action;
190
+ }
191
+
192
+ export async function pickReasoning(context) {
193
+ const result = await pick(`Choose Codex reasoning effort for ${context.model}`, REASONING_EFFORTS);
194
+ if (result.action === 'select') context.reasoningEffort = REASONING_EFFORTS[result.index];
195
+ return result.action;
196
+ }
197
+
198
+ export async function chooseLaunchContext(context) {
199
+ return await withPickerScreen(async () => {
200
+ let step = 'model';
201
+ while (true) {
202
+ if (step === 'model') {
203
+ const action = await pickModel(context);
204
+ if (action !== 'select') return false;
205
+ step = 'reasoning';
206
+ } else {
207
+ const action = await pickReasoning(context);
208
+ if (action === 'cancel') return false;
209
+ if (action === 'back') step = 'model';
210
+ else return true;
211
+ }
212
+ }
213
+ });
214
+ }
215
+
216
+ export async function newConversation(options) {
217
+ const context = createLaunchContext(options);
218
+ if (!context.model) {
219
+ print(missingModelMessage(context));
220
+ return;
221
+ }
222
+ const canPick = process.stdin.isTTY && process.stdout.isTTY;
223
+ const hasLaunchOverrides = options.provider || options.model || options.reasoningEffort;
224
+ if (!hasLaunchOverrides && !options.print && canPick) {
225
+ const selected = await chooseLaunchContext(context);
226
+ if (!selected) return;
227
+ }
228
+ if (options.print || (!hasLaunchOverrides && !canPick)) {
229
+ print(`${codexNewCommand(context)}\n`);
230
+ return;
231
+ }
232
+ await runCodex(codexNewArgs(context));
233
+ }
@@ -1,4 +1,3 @@
1
- import { spawn } from 'node:child_process';
2
1
  import {
3
2
  closeSync,
4
3
  existsSync,
@@ -7,27 +6,32 @@ import {
7
6
  readdirSync,
8
7
  readSync,
9
8
  } from 'node:fs';
10
- import { dirname, join, resolve } from 'node:path';
11
- import { clearLine, clearScreenDown, cursorTo } from 'node:readline';
12
- import { readCodexConfig } from './codex-config.js';
9
+ import { join, resolve } from 'node:path';
10
+ import {
11
+ codexNewArgs,
12
+ codexResumeArgs,
13
+ codexResumeCommand,
14
+ createLaunchContext,
15
+ missingModelMessage,
16
+ pick,
17
+ pickModel,
18
+ pickReasoning,
19
+ runCodex,
20
+ withPickerScreen,
21
+ } from './codex-launch.js';
13
22
 
14
- const DEFAULT_PROVIDER = 'deepseek-gateway';
15
- const REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
16
23
  const TIME_WIDTH = 10;
17
- const PROVIDER_WIDTH = 18;
24
+ const PROVIDER_WIDTH = 17;
18
25
  const ID_WIDTH = 36;
19
26
  const TITLE_WIDTH = 16;
20
27
  const TABLE_INDENT = ' ';
21
- const COLUMN_GAP = ' ';
28
+ const COLUMN_GAP = ' ';
29
+ const NEW_SESSION_ROW = '[New conversation]';
22
30
 
23
31
  function print(message = '') {
24
32
  process.stdout.write(message);
25
33
  }
26
34
 
27
- function defaultCodexHome() {
28
- return process.env.CODEX_HOME || join(process.env.USERPROFILE || process.env.HOME || process.cwd(), '.codex');
29
- }
30
-
31
35
  function readStart(file, bytes = 256 * 1024) {
32
36
  const handle = openSync(file, 'r');
33
37
  try {
@@ -107,16 +111,6 @@ function normalizePath(path) {
107
111
  return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
108
112
  }
109
113
 
110
- function findProjectRoot(start) {
111
- let current = resolve(start);
112
- while (true) {
113
- if (existsSync(join(current, '.git'))) return current;
114
- const parent = dirname(current);
115
- if (parent === current) return resolve(start);
116
- current = parent;
117
- }
118
- }
119
-
120
114
  function isInsideProject(cwd, projectRoot) {
121
115
  if (!cwd) return false;
122
116
  const sessionCwd = normalizePath(cwd);
@@ -138,31 +132,6 @@ function readSession(file, indexById) {
138
132
  };
139
133
  }
140
134
 
141
- function shellQuoteTomlString(key, value) {
142
- return `-c ${key}=${value}`;
143
- }
144
-
145
- function resumeCommand(sessionId, context) {
146
- return [
147
- 'codex',
148
- 'resume',
149
- sessionId,
150
- shellQuoteTomlString('model_provider', context.provider),
151
- shellQuoteTomlString('model', context.model),
152
- shellQuoteTomlString('model_reasoning_effort', context.reasoningEffort),
153
- ].join(' ');
154
- }
155
-
156
- function readJsonObject(file) {
157
- if (!existsSync(file)) return {};
158
- const value = JSON.parse(readFileSync(file, 'utf8'));
159
- return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
160
- }
161
-
162
- function gatewayModels(installDir) {
163
- return Object.keys(readJsonObject(join(installDir, 'config', 'model-aliases.json'))).sort();
164
- }
165
-
166
135
  function resolveSessionSelection(selection, sessionsList) {
167
136
  if (/^\d+$/.test(selection)) {
168
137
  const byIndex = sessionsList[Number(selection) - 1];
@@ -172,29 +141,6 @@ function resolveSessionSelection(selection, sessionsList) {
172
141
  return matches.length === 1 ? matches[0] : null;
173
142
  }
174
143
 
175
- function resumeArgs(session, context) {
176
- return [
177
- 'resume',
178
- session.id,
179
- '-c',
180
- `model_provider=${JSON.stringify(context.provider)}`,
181
- '-c',
182
- `model=${JSON.stringify(context.model)}`,
183
- '-c',
184
- `model_reasoning_effort=${JSON.stringify(context.reasoningEffort)}`,
185
- ];
186
- }
187
-
188
- async function runCodexResume(session, context) {
189
- const child = spawn('codex', resumeArgs(session, context), {
190
- stdio: 'inherit',
191
- shell: process.platform === 'win32',
192
- });
193
- process.exitCode = await new Promise((resolveChild) => {
194
- child.on('exit', (exitCode) => resolveChild(exitCode ?? 0));
195
- });
196
- }
197
-
198
144
  function printSessions(sessionsList, options, context) {
199
145
  const listed = sessionsList.slice(0, options.limit || 20);
200
146
  print(`Codex sessions ${options.all ? `under ${context.codexHome}` : `for project ${context.projectRoot}`}\n`);
@@ -204,11 +150,11 @@ function printSessions(sessionsList, options, context) {
204
150
  return;
205
151
  }
206
152
  print(`${TABLE_INDENT}${sessionHeader()}\n`);
207
- print(`${TABLE_INDENT}${'-'.repeat(TIME_WIDTH)}${COLUMN_GAP}${'-'.repeat(PROVIDER_WIDTH)}${COLUMN_GAP}${'-'.repeat(ID_WIDTH)}${COLUMN_GAP}${'-'.repeat(TITLE_WIDTH)}\n`);
153
+ print(`${TABLE_INDENT}${'-'.repeat(sessionHeader().length)}\n`);
208
154
  listed.forEach((session, index) => {
209
155
  print(`${String(index + 1).padStart(2, ' ')} ${sessionRow(session)}\n`);
210
156
  if (options.all) print(` cwd: ${session.cwd || '(unknown cwd)'}\n`);
211
- print(` resume: ${resumeCommand(session.id, context)}\n\n`);
157
+ print(` resume: ${codexResumeCommand(session.id, context)}\n\n`);
212
158
  });
213
159
  if (sessionsList.length > listed.length) print(`Showing ${listed.length} of ${sessionsList.length}. Use --limit ${sessionsList.length} or --all as needed.\n`);
214
160
  }
@@ -225,134 +171,47 @@ function sessionRows(sessionsList) {
225
171
  return sessionsList.map((session) => sessionRow(session));
226
172
  }
227
173
 
228
- function rowOffset(header) {
229
- return header ? 4 : 2;
230
- }
231
-
232
- function renderRow(state, index) {
233
- cursorTo(process.stdout, 0, rowOffset(state.header) + index);
234
- clearLine(process.stdout, 0);
235
- print(`${index === state.selected ? '>' : ' '} ${state.rows[index]}`);
236
- }
237
-
238
- function renderPicker(state) {
239
- const { title, rows, selected, header } = state;
240
- cursorTo(process.stdout, 0, 0);
241
- clearScreenDown(process.stdout);
242
- let output = `${title}\n\n`;
243
- if (header) output += ` ${header}\n ${'-'.repeat(header.length)}\n`;
244
- for (const [index, row] of rows.entries()) output += `${index === selected ? '>' : ' '} ${row}\n`;
245
- output += '\n↑/↓ select Enter confirm ← back Esc quit\n';
246
- print(output);
247
- }
248
-
249
- function openPickerScreen() {
250
- process.stdout.write('\x1b[?1049h\x1b[?25l');
251
- }
252
-
253
- function closePickerScreen() {
254
- process.stdout.write('\x1b[?25h\x1b[?1049l');
255
- }
256
-
257
- async function pick(title, rows, header = '') {
258
- if (!rows.length) return { action: 'back' };
259
- let selected = 0;
260
- const stdin = process.stdin;
261
- const state = { title, rows, selected, header };
262
- renderPicker(state);
263
-
264
- return await new Promise((resolvePick) => {
265
- const done = (result) => {
266
- stdin.off('data', onData);
267
- resolvePick(result);
268
- };
269
- const onData = (chunk) => {
270
- const key = chunk.toString('utf8');
271
- if (key === '\u0003' || key === '\u001b') return done({ action: 'cancel' });
272
- if (key === '\u001b[D') return done({ action: 'back' });
273
- if (key === '\r' || key === '\n') return done({ action: 'select', index: selected });
274
- const previous = selected;
275
- if (key === '\u001b[A') selected = Math.max(0, selected - 1);
276
- else if (key === '\u001b[B') selected = Math.min(rows.length - 1, selected + 1);
277
- else return;
278
- if (selected === previous) return;
279
- state.selected = selected;
280
- renderRow(state, previous);
281
- renderRow(state, selected);
282
- };
283
- stdin.on('data', onData);
284
- });
285
- }
286
-
287
174
  async function chooseSessionFlow(allSessions, context, limit) {
288
- const models = gatewayModels(context.installDir);
289
- const listed = allSessions.slice(0, limit);
290
- const header = sessionHeader();
291
- if (!listed.length) {
292
- print('No matching sessions found.\n');
293
- return null;
294
- }
295
- if (!models.length) {
296
- print(`No gateway models found in ${join(context.installDir, 'config', 'model-aliases.json')}\n`);
175
+ if (!context.models.length) {
176
+ print(missingModelMessage(context));
297
177
  return null;
298
178
  }
299
179
 
300
- const stdin = process.stdin;
301
- stdin.resume();
302
- stdin.setRawMode(true);
303
- openPickerScreen();
304
- try {
180
+ const listed = allSessions.slice(0, limit);
181
+ const header = sessionHeader();
182
+ return await withPickerScreen(async () => {
305
183
  let step = 'model';
306
184
  while (true) {
307
185
  if (step === 'model') {
308
- const result = await pick('Choose gateway model', models);
309
- if (result.action !== 'select') return null;
310
- context.model = models[result.index];
186
+ const action = await pickModel(context);
187
+ if (action !== 'select') return null;
311
188
  step = 'reasoning';
312
189
  } else if (step === 'reasoning') {
313
- const result = await pick(`Choose Codex reasoning effort for ${context.model}`, REASONING_EFFORTS);
314
- if (result.action === 'cancel') return null;
315
- if (result.action === 'back') step = 'model';
316
- else {
317
- context.reasoningEffort = REASONING_EFFORTS[result.index];
318
- step = 'session';
319
- }
190
+ const action = await pickReasoning(context);
191
+ if (action === 'cancel') return null;
192
+ if (action === 'back') step = 'model';
193
+ else step = 'session';
320
194
  } else {
321
- const result = await pick(`Choose Codex session for ${context.provider} / ${context.model} / ${context.reasoningEffort}`, sessionRows(listed), header);
195
+ const rows = [NEW_SESSION_ROW, ...sessionRows(listed)];
196
+ const result = await pick(`Choose Codex session for ${context.provider} / ${context.model} / ${context.reasoningEffort}`, rows, header);
322
197
  if (result.action === 'cancel') return null;
323
198
  if (result.action === 'back') step = 'reasoning';
324
- else return listed[result.index];
199
+ else if (result.index === 0) return { newConversation: true };
200
+ else return listed[result.index - 1];
325
201
  }
326
202
  }
327
- } finally {
328
- stdin.setRawMode(false);
329
- stdin.pause();
330
- closePickerScreen();
331
- }
203
+ });
332
204
  }
333
205
 
334
206
  export async function sessions(options) {
335
- const codexHome = defaultCodexHome();
336
- const sessionsDir = join(codexHome, 'sessions');
337
- const indexById = readSessionIndex(codexHome);
338
- const projectRoot = findProjectRoot(process.cwd());
339
- const codexConfig = readCodexConfig();
340
- const models = gatewayModels(options.dir);
341
- const configModel = codexConfig.modelProvider === DEFAULT_PROVIDER && models.includes(codexConfig.model) ? codexConfig.model : '';
342
- const context = {
343
- all: options.all,
344
- codexHome,
345
- installDir: options.dir,
346
- projectRoot,
347
- provider: options.provider || DEFAULT_PROVIDER,
348
- model: options.model || configModel || models[0] || '',
349
- reasoningEffort: options.reasoningEffort || codexConfig.modelReasoningEffort || 'low',
350
- };
207
+ const context = createLaunchContext(options);
208
+ const sessionsDir = join(context.codexHome, 'sessions');
209
+ const indexById = readSessionIndex(context.codexHome);
351
210
  const seen = new Map();
352
211
 
353
212
  for (const file of walkJsonl(sessionsDir)) {
354
213
  const session = readSession(file, indexById);
355
- if (!session || (!options.all && !isInsideProject(session.cwd, projectRoot))) continue;
214
+ if (!session || (!options.all && !isInsideProject(session.cwd, context.projectRoot))) continue;
356
215
  const prior = seen.get(session.id);
357
216
  if (!prior || session.updatedAt > prior.updatedAt) seen.set(session.id, session);
358
217
  }
@@ -361,7 +220,7 @@ export async function sessions(options) {
361
220
  if (options.exec) {
362
221
  const session = resolveSessionSelection(options.exec, allSessions);
363
222
  if (!session) throw new Error(`Session not found or ambiguous: ${options.exec}`);
364
- await runCodexResume(session, context);
223
+ await runCodex(codexResumeArgs(session.id, context));
365
224
  return;
366
225
  }
367
226
 
@@ -370,6 +229,7 @@ export async function sessions(options) {
370
229
  return;
371
230
  }
372
231
 
373
- const session = await chooseSessionFlow(allSessions, context, options.limit || 20);
374
- if (session) await runCodexResume(session, context);
232
+ const selected = await chooseSessionFlow(allSessions, context, options.limit || 20);
233
+ if (selected?.newConversation) await runCodex(codexNewArgs(context));
234
+ else if (selected) await runCodex(codexResumeArgs(selected.id, context));
375
235
  }