@galaxy-yearn/codex-deepseek-gateway 0.1.4 → 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,25 +70,22 @@ 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',
66
77
  `model=${JSON.stringify(context.model)}`,
67
78
  '-c',
68
79
  `model_reasoning_effort=${JSON.stringify(context.reasoningEffort)}`,
69
- ];
70
- }
71
-
72
- function configOverrideCommandParts(context) {
73
- return [
74
- '-c',
75
- `model_provider=${context.provider}`,
76
80
  '-c',
77
- `model=${context.model}`,
81
+ 'model_supports_reasoning_summaries=true',
78
82
  '-c',
79
- `model_reasoning_effort=${context.reasoningEffort}`,
83
+ 'model_reasoning_summary="auto"',
80
84
  ];
85
+ if (context.provider === DEFAULT_PROVIDER && context.modelCatalogPath) {
86
+ args.push('-c', `model_catalog_json=${JSON.stringify(context.modelCatalogPath)}`);
87
+ }
88
+ return args;
81
89
  }
82
90
 
83
91
  export function codexNewArgs(context) {
@@ -88,20 +96,146 @@ export function codexResumeArgs(sessionId, context) {
88
96
  return ['resume', sessionId, ...configOverrideArgs(context)];
89
97
  }
90
98
 
91
- export function codexNewCommand(context) {
92
- 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, `'"'"'`)}'`;
93
103
  }
94
104
 
95
105
  export function codexResumeCommand(sessionId, context) {
96
- 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
+ };
97
226
  }
98
227
 
99
228
  export async function runCodex(args) {
100
- const child = spawn('codex', args, {
229
+ const child = spawn(resolveCodexExecutable(), args, {
101
230
  stdio: 'inherit',
102
- shell: process.platform === 'win32',
231
+ shell: false,
232
+ env: codexProcessEnv(),
103
233
  });
104
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
+ });
105
239
  child.on('exit', (exitCode) => resolveChild(exitCode ?? 0));
106
240
  });
107
241
  }
@@ -110,25 +244,73 @@ function rowOffset(header) {
110
244
  return header ? 4 : 2;
111
245
  }
112
246
 
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;
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;
117
275
  print(`\x1b[2K${styled}`);
118
276
  }
119
277
 
120
- function renderPicker(state) {
121
- 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);
122
300
  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`;
301
+ if (clear) {
302
+ clearScreenDown(process.stdout);
303
+ print(`${lines.join('\n')}\n`);
304
+ state.renderedLineCount = lines.length;
305
+ return;
129
306
  }
130
- output += '\n↑/↓ select Enter confirm ← back Esc quit\n';
131
- print(output);
307
+ for (const line of lines) {
308
+ print(`\x1b[2K${line}\n`);
309
+ }
310
+ for (let index = lines.length; index < (state.renderedLineCount || 0); index += 1) {
311
+ print('\x1b[2K\n');
312
+ }
313
+ state.renderedLineCount = lines.length;
132
314
  }
133
315
 
134
316
  function openPickerScreen() {
@@ -139,11 +321,23 @@ function closePickerScreen() {
139
321
  process.stdout.write('\x1b[?25h\x1b[?1049l');
140
322
  }
141
323
 
142
- export async function pick(title, rows, header = '') {
143
- 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' };
144
327
  let selected = 0;
145
328
  const stdin = process.stdin;
146
- 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
+ };
147
341
  renderPicker(state);
148
342
 
149
343
  return await new Promise((resolvePick) => {
@@ -155,13 +349,25 @@ export async function pick(title, rows, header = '') {
155
349
  const key = chunk.toString('utf8');
156
350
  if (key === '\u0003' || key === '\u001b') return done({ action: 'cancel' });
157
351
  if (key === '\u001b[D') return done({ action: 'back' });
158
- 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;
159
359
  const previous = selected;
360
+ const previousOffset = state.offset;
160
361
  if (key === '\u001b[A') selected = Math.max(0, selected - 1);
161
362
  else if (key === '\u001b[B') selected = Math.min(rows.length - 1, selected + 1);
162
363
  else return;
163
364
  if (selected === previous) return;
164
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
+ }
165
371
  renderRow(state, previous);
166
372
  renderRow(state, selected);
167
373
  };
@@ -214,6 +420,7 @@ export async function chooseLaunchContext(context) {
214
420
  }
215
421
 
216
422
  export async function newConversation(options) {
423
+ if (options.print) throw new Error('--print is only supported with sessions');
217
424
  const context = createLaunchContext(options);
218
425
  if (!context.model) {
219
426
  print(missingModelMessage(context));
@@ -225,9 +432,5 @@ export async function newConversation(options) {
225
432
  const selected = await chooseLaunchContext(context);
226
433
  if (!selected) return;
227
434
  }
228
- if (options.print || (!hasLaunchOverrides && !canPick)) {
229
- print(`${codexNewCommand(context)}\n`);
230
- return;
231
- }
232
435
  await runCodex(codexNewArgs(context));
233
436
  }
@@ -5,6 +5,7 @@ import {
5
5
  readFileSync,
6
6
  readdirSync,
7
7
  readSync,
8
+ statSync,
8
9
  } from 'node:fs';
9
10
  import { join, resolve } from 'node:path';
10
11
  import {
@@ -26,7 +27,7 @@ const ID_WIDTH = 36;
26
27
  const TITLE_WIDTH = 16;
27
28
  const TABLE_INDENT = ' ';
28
29
  const COLUMN_GAP = ' ';
29
- const NEW_SESSION_ROW = '[New conversation]';
30
+ export const DEFAULT_SESSION_LIMIT = 15;
30
31
 
31
32
  function print(message = '') {
32
33
  process.stdout.write(message);
@@ -60,6 +61,48 @@ function parseJsonLine(line) {
60
61
  }
61
62
  }
62
63
 
64
+ function isSubagentSession(meta) {
65
+ return meta?.thread_source === 'subagent' || Boolean(meta?.parent_thread_id);
66
+ }
67
+
68
+ function timestampFromUserMessage(row) {
69
+ const payload = row?.payload;
70
+ if (row?.type === 'event_msg' && payload?.type === 'user_message') return row.timestamp || '';
71
+ if (row?.type === 'response_item' && payload?.type === 'message' && payload.role === 'user') return row.timestamp || '';
72
+ return '';
73
+ }
74
+
75
+ function readLastUserMessageTimestamp(file, chunkSize = 256 * 1024) {
76
+ const handle = openSync(file, 'r');
77
+ try {
78
+ let position = statSync(file).size;
79
+ let suffix = Buffer.alloc(0);
80
+ while (position > 0) {
81
+ const length = Math.min(chunkSize, position);
82
+ position -= length;
83
+ const buffer = Buffer.alloc(length);
84
+ const read = readSync(handle, buffer, 0, length, position);
85
+ let data = buffer.subarray(0, read);
86
+ if (suffix.length) data = Buffer.concat([data, suffix]);
87
+
88
+ let lineEnd = data.length;
89
+ for (let index = data.length - 1; index >= 0; index -= 1) {
90
+ if (data[index] !== 10) continue;
91
+ const row = parseJsonLine(data.subarray(index + 1, lineEnd).toString('utf8').trim());
92
+ const timestamp = timestampFromUserMessage(row);
93
+ if (timestamp) return timestamp;
94
+ lineEnd = index;
95
+ }
96
+ suffix = data.subarray(0, lineEnd);
97
+ }
98
+
99
+ const row = parseJsonLine(suffix.toString('utf8').trim());
100
+ return timestampFromUserMessage(row);
101
+ } finally {
102
+ closeSync(handle);
103
+ }
104
+ }
105
+
63
106
  function readSessionIndex(codexHome) {
64
107
  const file = join(codexHome, 'session_index.jsonl');
65
108
  const byId = new Map();
@@ -118,16 +161,17 @@ function isInsideProject(cwd, projectRoot) {
118
161
  return sessionCwd === root || sessionCwd.startsWith(`${root}${process.platform === 'win32' ? '\\' : '/'}`);
119
162
  }
120
163
 
121
- function readSession(file, indexById) {
164
+ export function readSession(file, indexById) {
122
165
  const lines = readStart(file).split(/\r?\n/).filter(Boolean);
123
166
  const meta = parseJsonLine(lines[0])?.payload;
124
167
  if (!meta?.id) return null;
168
+ if (isSubagentSession(meta)) return null;
125
169
  const index = indexById.get(meta.id) || {};
126
170
  return {
127
171
  id: meta.id,
128
172
  provider: meta.model_provider || '',
129
173
  cwd: meta.cwd || '',
130
- updatedAt: index.updated_at || meta.timestamp || '',
174
+ updatedAt: readLastUserMessageTimestamp(file) || index.updated_at || meta.timestamp || '',
131
175
  title: index.thread_name || firstUserPreview(lines) || '(untitled)',
132
176
  };
133
177
  }
@@ -142,7 +186,7 @@ function resolveSessionSelection(selection, sessionsList) {
142
186
  }
143
187
 
144
188
  function printSessions(sessionsList, options, context) {
145
- const listed = sessionsList.slice(0, options.limit || 20);
189
+ const listed = sessionsList.slice(0, options.limit || DEFAULT_SESSION_LIMIT);
146
190
  print(`Codex sessions ${options.all ? `under ${context.codexHome}` : `for project ${context.projectRoot}`}\n`);
147
191
  print(`Target: ${context.provider} / ${context.model} / ${context.reasoningEffort}\n\n`);
148
192
  if (!listed.length) {
@@ -171,18 +215,37 @@ function sessionRows(sessionsList) {
171
215
  return sessionsList.map((session) => sessionRow(session));
172
216
  }
173
217
 
218
+ export function sessionPickerRows(allSessions) {
219
+ return sessionRows(allSessions);
220
+ }
221
+
174
222
  async function chooseSessionFlow(allSessions, context, limit) {
175
223
  if (!context.models.length) {
176
224
  print(missingModelMessage(context));
177
225
  return null;
178
226
  }
179
227
 
180
- const listed = allSessions.slice(0, limit);
181
228
  const header = sessionHeader();
229
+ const windowSize = limit || DEFAULT_SESSION_LIMIT;
182
230
  return await withPickerScreen(async () => {
183
- let step = 'model';
231
+ let step = 'session';
184
232
  while (true) {
185
- if (step === 'model') {
233
+ if (step === 'session') {
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
+ );
244
+ if (result.action === 'cancel') return null;
245
+ if (result.action === 'back') return null;
246
+ context.selectedSession = result.action === 'new' ? { newConversation: true } : allSessions[result.index];
247
+ step = 'model';
248
+ } else if (step === 'model') {
186
249
  const action = await pickModel(context);
187
250
  if (action !== 'select') return null;
188
251
  step = 'reasoning';
@@ -190,14 +253,7 @@ async function chooseSessionFlow(allSessions, context, limit) {
190
253
  const action = await pickReasoning(context);
191
254
  if (action === 'cancel') return null;
192
255
  if (action === 'back') step = 'model';
193
- else step = 'session';
194
- } else {
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);
197
- if (result.action === 'cancel') return null;
198
- if (result.action === 'back') step = 'reasoning';
199
- else if (result.index === 0) return { newConversation: true };
200
- else return listed[result.index - 1];
256
+ else return context.selectedSession;
201
257
  }
202
258
  }
203
259
  });
@@ -229,7 +285,7 @@ export async function sessions(options) {
229
285
  return;
230
286
  }
231
287
 
232
- const selected = await chooseSessionFlow(allSessions, context, options.limit || 20);
288
+ const selected = await chooseSessionFlow(allSessions, context, options.limit || DEFAULT_SESSION_LIMIT);
233
289
  if (selected?.newConversation) await runCodex(codexNewArgs(context));
234
290
  else if (selected) await runCodex(codexResumeArgs(selected.id, context));
235
291
  }
package/src/config.js CHANGED
@@ -25,7 +25,7 @@ export function loadConfig(env = process.env) {
25
25
  tavilyBaseUrl: mergedEnv.TAVILY_BASE_URL || 'https://api.tavily.com',
26
26
  tavilySearchDepth: mergedEnv.TAVILY_SEARCH_DEPTH || 'basic',
27
27
  tavilyMaxResults: Number(mergedEnv.TAVILY_MAX_RESULTS || 5),
28
- tavilyMaxSearchRounds: Number(mergedEnv.TAVILY_MAX_SEARCH_ROUNDS || 2),
28
+ tavilyMaxSearchRounds: Number(mergedEnv.TAVILY_MAX_SEARCH_ROUNDS || 10),
29
29
  tavilyTimeoutMs: Number(mergedEnv.TAVILY_TIMEOUT_MS || 15000),
30
30
  tavilySnippetChars: Number(mergedEnv.TAVILY_SNIPPET_CHARS || 650),
31
31
  tavilyResultMaxChars: Number(mergedEnv.TAVILY_RESULT_MAX_CHARS || 6000),
package/src/firecrawl.js CHANGED
@@ -304,12 +304,12 @@ export function formatFirecrawlScrapeResult({ url = '', title = '', summary = ''
304
304
  if (links?.length) {
305
305
  lines.push('Page links:');
306
306
  for (const [index, link] of links.entries()) {
307
- lines.push(`[L${index + 1}] ${link.title || link.url}`);
307
+ lines.push(`Link ${index + 1}: ${link.title || link.url}`);
308
308
  lines.push(`URL: ${link.url}`);
309
309
  }
310
310
  }
311
311
  }
312
- lines.push('Use this opened page only as source text. Cite it with the source number assigned by the search result when available.');
312
+ lines.push('Use this opened page only as source text. If it supports the answer, include the page title and URL.');
313
313
  const text = lines.filter(Boolean).join('\n');
314
314
  const maxChars = Number(config.firecrawlResultMaxChars) || DEFAULT_TOTAL_CHARS;
315
315
  if (text.length <= maxChars) return text;