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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,241 @@
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
+ '-c',
70
+ 'model_supports_reasoning_summaries=true',
71
+ '-c',
72
+ 'model_reasoning_summary="auto"',
73
+ ];
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
+ ];
89
+ }
90
+
91
+ export function codexNewArgs(context) {
92
+ return configOverrideArgs(context);
93
+ }
94
+
95
+ export function codexResumeArgs(sessionId, context) {
96
+ return ['resume', sessionId, ...configOverrideArgs(context)];
97
+ }
98
+
99
+ export function codexNewCommand(context) {
100
+ return ['codex', ...configOverrideCommandParts(context)].join(' ');
101
+ }
102
+
103
+ export function codexResumeCommand(sessionId, context) {
104
+ return ['codex', 'resume', sessionId, ...configOverrideCommandParts(context)].join(' ');
105
+ }
106
+
107
+ export async function runCodex(args) {
108
+ const child = spawn('codex', args, {
109
+ stdio: 'inherit',
110
+ shell: process.platform === 'win32',
111
+ });
112
+ process.exitCode = await new Promise((resolveChild) => {
113
+ child.on('exit', (exitCode) => resolveChild(exitCode ?? 0));
114
+ });
115
+ }
116
+
117
+ function rowOffset(header) {
118
+ return header ? 4 : 2;
119
+ }
120
+
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;
125
+ print(`\x1b[2K${styled}`);
126
+ }
127
+
128
+ function renderPicker(state) {
129
+ const { title, rows, selected, header } = state;
130
+ 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`;
137
+ }
138
+ output += '\nUp/Down select Enter confirm Left back Esc quit\n';
139
+ print(output);
140
+ }
141
+
142
+ function openPickerScreen() {
143
+ process.stdout.write('\x1b[?1049h\x1b[?25l');
144
+ }
145
+
146
+ function closePickerScreen() {
147
+ process.stdout.write('\x1b[?25h\x1b[?1049l');
148
+ }
149
+
150
+ export async function pick(title, rows, header = '') {
151
+ if (!rows.length) return { action: 'back' };
152
+ let selected = 0;
153
+ const stdin = process.stdin;
154
+ const state = { title, rows, selected, header };
155
+ renderPicker(state);
156
+
157
+ return await new Promise((resolvePick) => {
158
+ const done = (result) => {
159
+ stdin.off('data', onData);
160
+ resolvePick(result);
161
+ };
162
+ const onData = (chunk) => {
163
+ const key = chunk.toString('utf8');
164
+ if (key === '\u0003' || key === '\u001b') return done({ action: 'cancel' });
165
+ if (key === '\u001b[D') return done({ action: 'back' });
166
+ if (key === '\r' || key === '\n') return done({ action: 'select', index: selected });
167
+ const previous = selected;
168
+ if (key === '\u001b[A') selected = Math.max(0, selected - 1);
169
+ else if (key === '\u001b[B') selected = Math.min(rows.length - 1, selected + 1);
170
+ else return;
171
+ if (selected === previous) return;
172
+ state.selected = selected;
173
+ renderRow(state, previous);
174
+ renderRow(state, selected);
175
+ };
176
+ stdin.on('data', onData);
177
+ });
178
+ }
179
+
180
+ export async function withPickerScreen(callback) {
181
+ const stdin = process.stdin;
182
+ stdin.resume();
183
+ stdin.setRawMode(true);
184
+ openPickerScreen();
185
+ try {
186
+ return await callback();
187
+ } finally {
188
+ stdin.setRawMode(false);
189
+ stdin.pause();
190
+ closePickerScreen();
191
+ }
192
+ }
193
+
194
+ export async function pickModel(context) {
195
+ const result = await pick('Choose gateway model', context.models);
196
+ if (result.action === 'select') context.model = context.models[result.index];
197
+ return result.action;
198
+ }
199
+
200
+ export async function pickReasoning(context) {
201
+ const result = await pick(`Choose Codex reasoning effort for ${context.model}`, REASONING_EFFORTS);
202
+ if (result.action === 'select') context.reasoningEffort = REASONING_EFFORTS[result.index];
203
+ return result.action;
204
+ }
205
+
206
+ export async function chooseLaunchContext(context) {
207
+ return await withPickerScreen(async () => {
208
+ let step = 'model';
209
+ while (true) {
210
+ if (step === 'model') {
211
+ const action = await pickModel(context);
212
+ if (action !== 'select') return false;
213
+ step = 'reasoning';
214
+ } else {
215
+ const action = await pickReasoning(context);
216
+ if (action === 'cancel') return false;
217
+ if (action === 'back') step = 'model';
218
+ else return true;
219
+ }
220
+ }
221
+ });
222
+ }
223
+
224
+ export async function newConversation(options) {
225
+ const context = createLaunchContext(options);
226
+ if (!context.model) {
227
+ print(missingModelMessage(context));
228
+ return;
229
+ }
230
+ const canPick = process.stdin.isTTY && process.stdout.isTTY;
231
+ const hasLaunchOverrides = options.provider || options.model || options.reasoningEffort;
232
+ if (!hasLaunchOverrides && !options.print && canPick) {
233
+ const selected = await chooseLaunchContext(context);
234
+ if (!selected) return;
235
+ }
236
+ if (options.print || (!hasLaunchOverrides && !canPick)) {
237
+ print(`${codexNewCommand(context)}\n`);
238
+ return;
239
+ }
240
+ await runCodex(codexNewArgs(context));
241
+ }
@@ -1,4 +1,3 @@
1
- import { spawn } from 'node:child_process';
2
1
  import {
3
2
  closeSync,
4
3
  existsSync,
@@ -6,28 +5,34 @@ import {
6
5
  readFileSync,
7
6
  readdirSync,
8
7
  readSync,
8
+ statSync,
9
9
  } 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';
10
+ import { join, resolve } from 'node:path';
11
+ import {
12
+ codexNewArgs,
13
+ codexResumeArgs,
14
+ codexResumeCommand,
15
+ createLaunchContext,
16
+ missingModelMessage,
17
+ pick,
18
+ pickModel,
19
+ pickReasoning,
20
+ runCodex,
21
+ withPickerScreen,
22
+ } from './codex-launch.js';
13
23
 
14
- const DEFAULT_PROVIDER = 'deepseek-gateway';
15
- const REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
16
24
  const TIME_WIDTH = 10;
17
- const PROVIDER_WIDTH = 18;
25
+ const PROVIDER_WIDTH = 17;
18
26
  const ID_WIDTH = 36;
19
27
  const TITLE_WIDTH = 16;
20
28
  const TABLE_INDENT = ' ';
21
- const COLUMN_GAP = ' ';
29
+ const COLUMN_GAP = ' ';
30
+ const NEW_SESSION_ROW = '[New conversation]';
22
31
 
23
32
  function print(message = '') {
24
33
  process.stdout.write(message);
25
34
  }
26
35
 
27
- function defaultCodexHome() {
28
- return process.env.CODEX_HOME || join(process.env.USERPROFILE || process.env.HOME || process.cwd(), '.codex');
29
- }
30
-
31
36
  function readStart(file, bytes = 256 * 1024) {
32
37
  const handle = openSync(file, 'r');
33
38
  try {
@@ -56,6 +61,48 @@ function parseJsonLine(line) {
56
61
  }
57
62
  }
58
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
+
59
106
  function readSessionIndex(codexHome) {
60
107
  const file = join(codexHome, 'session_index.jsonl');
61
108
  const byId = new Map();
@@ -107,16 +154,6 @@ function normalizePath(path) {
107
154
  return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
108
155
  }
109
156
 
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
157
  function isInsideProject(cwd, projectRoot) {
121
158
  if (!cwd) return false;
122
159
  const sessionCwd = normalizePath(cwd);
@@ -124,45 +161,21 @@ function isInsideProject(cwd, projectRoot) {
124
161
  return sessionCwd === root || sessionCwd.startsWith(`${root}${process.platform === 'win32' ? '\\' : '/'}`);
125
162
  }
126
163
 
127
- function readSession(file, indexById) {
164
+ export function readSession(file, indexById) {
128
165
  const lines = readStart(file).split(/\r?\n/).filter(Boolean);
129
166
  const meta = parseJsonLine(lines[0])?.payload;
130
167
  if (!meta?.id) return null;
168
+ if (isSubagentSession(meta)) return null;
131
169
  const index = indexById.get(meta.id) || {};
132
170
  return {
133
171
  id: meta.id,
134
172
  provider: meta.model_provider || '',
135
173
  cwd: meta.cwd || '',
136
- updatedAt: index.updated_at || meta.timestamp || '',
174
+ updatedAt: readLastUserMessageTimestamp(file) || index.updated_at || meta.timestamp || '',
137
175
  title: index.thread_name || firstUserPreview(lines) || '(untitled)',
138
176
  };
139
177
  }
140
178
 
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
179
  function resolveSessionSelection(selection, sessionsList) {
167
180
  if (/^\d+$/.test(selection)) {
168
181
  const byIndex = sessionsList[Number(selection) - 1];
@@ -172,29 +185,6 @@ function resolveSessionSelection(selection, sessionsList) {
172
185
  return matches.length === 1 ? matches[0] : null;
173
186
  }
174
187
 
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
188
  function printSessions(sessionsList, options, context) {
199
189
  const listed = sessionsList.slice(0, options.limit || 20);
200
190
  print(`Codex sessions ${options.all ? `under ${context.codexHome}` : `for project ${context.projectRoot}`}\n`);
@@ -204,11 +194,11 @@ function printSessions(sessionsList, options, context) {
204
194
  return;
205
195
  }
206
196
  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`);
197
+ print(`${TABLE_INDENT}${'-'.repeat(sessionHeader().length)}\n`);
208
198
  listed.forEach((session, index) => {
209
199
  print(`${String(index + 1).padStart(2, ' ')} ${sessionRow(session)}\n`);
210
200
  if (options.all) print(` cwd: ${session.cwd || '(unknown cwd)'}\n`);
211
- print(` resume: ${resumeCommand(session.id, context)}\n\n`);
201
+ print(` resume: ${codexResumeCommand(session.id, context)}\n\n`);
212
202
  });
213
203
  if (sessionsList.length > listed.length) print(`Showing ${listed.length} of ${sessionsList.length}. Use --limit ${sessionsList.length} or --all as needed.\n`);
214
204
  }
@@ -225,134 +215,47 @@ function sessionRows(sessionsList) {
225
215
  return sessionsList.map((session) => sessionRow(session));
226
216
  }
227
217
 
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
218
  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`);
219
+ if (!context.models.length) {
220
+ print(missingModelMessage(context));
297
221
  return null;
298
222
  }
299
223
 
300
- const stdin = process.stdin;
301
- stdin.resume();
302
- stdin.setRawMode(true);
303
- openPickerScreen();
304
- try {
305
- let step = 'model';
224
+ const listed = allSessions.slice(0, limit);
225
+ const header = sessionHeader();
226
+ return await withPickerScreen(async () => {
227
+ let step = 'session';
306
228
  while (true) {
307
- 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];
229
+ if (step === 'session') {
230
+ const rows = [NEW_SESSION_ROW, ...sessionRows(listed)];
231
+ const result = await pick(`Choose Codex session`, rows, header);
232
+ if (result.action === 'cancel') return null;
233
+ if (result.action === 'back') return null;
234
+ context.selectedSession = result.index === 0 ? { newConversation: true } : listed[result.index - 1];
235
+ step = 'model';
236
+ } else if (step === 'model') {
237
+ const action = await pickModel(context);
238
+ if (action !== 'select') return null;
311
239
  step = 'reasoning';
312
240
  } 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
- }
320
- } else {
321
- const result = await pick(`Choose Codex session for ${context.provider} / ${context.model} / ${context.reasoningEffort}`, sessionRows(listed), header);
322
- if (result.action === 'cancel') return null;
323
- if (result.action === 'back') step = 'reasoning';
324
- else return listed[result.index];
241
+ const action = await pickReasoning(context);
242
+ if (action === 'cancel') return null;
243
+ if (action === 'back') step = 'model';
244
+ else return context.selectedSession;
325
245
  }
326
246
  }
327
- } finally {
328
- stdin.setRawMode(false);
329
- stdin.pause();
330
- closePickerScreen();
331
- }
247
+ });
332
248
  }
333
249
 
334
250
  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
- };
251
+ const context = createLaunchContext(options);
252
+ const sessionsDir = join(context.codexHome, 'sessions');
253
+ const indexById = readSessionIndex(context.codexHome);
351
254
  const seen = new Map();
352
255
 
353
256
  for (const file of walkJsonl(sessionsDir)) {
354
257
  const session = readSession(file, indexById);
355
- if (!session || (!options.all && !isInsideProject(session.cwd, projectRoot))) continue;
258
+ if (!session || (!options.all && !isInsideProject(session.cwd, context.projectRoot))) continue;
356
259
  const prior = seen.get(session.id);
357
260
  if (!prior || session.updatedAt > prior.updatedAt) seen.set(session.id, session);
358
261
  }
@@ -361,7 +264,7 @@ export async function sessions(options) {
361
264
  if (options.exec) {
362
265
  const session = resolveSessionSelection(options.exec, allSessions);
363
266
  if (!session) throw new Error(`Session not found or ambiguous: ${options.exec}`);
364
- await runCodexResume(session, context);
267
+ await runCodex(codexResumeArgs(session.id, context));
365
268
  return;
366
269
  }
367
270
 
@@ -370,6 +273,7 @@ export async function sessions(options) {
370
273
  return;
371
274
  }
372
275
 
373
- const session = await chooseSessionFlow(allSessions, context, options.limit || 20);
374
- if (session) await runCodexResume(session, context);
276
+ const selected = await chooseSessionFlow(allSessions, context, options.limit || 20);
277
+ if (selected?.newConversation) await runCodex(codexNewArgs(context));
278
+ else if (selected) await runCodex(codexResumeArgs(selected.id, context));
375
279
  }
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;