@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.
- package/README.md +86 -157
- package/bin/codex-deepseek-gateway.js +20 -9
- package/config/gateway.example.json +1 -17
- package/package.json +2 -2
- package/src/codex-launch.js +241 -0
- package/src/codex-sessions.js +93 -189
- package/src/config.js +1 -1
- package/src/firecrawl.js +2 -2
- package/src/protocol.js +331 -86
- package/src/server.js +125 -15
- package/src/tavily.js +3 -3
- package/src/web-search-emulator.js +104 -23
|
@@ -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
|
+
}
|
package/src/codex-sessions.js
CHANGED
|
@@ -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 {
|
|
11
|
-
import {
|
|
12
|
-
|
|
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 =
|
|
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(
|
|
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: ${
|
|
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
|
-
|
|
289
|
-
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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 === '
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
314
|
-
if (
|
|
315
|
-
if (
|
|
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
|
-
}
|
|
328
|
-
stdin.setRawMode(false);
|
|
329
|
-
stdin.pause();
|
|
330
|
-
closePickerScreen();
|
|
331
|
-
}
|
|
247
|
+
});
|
|
332
248
|
}
|
|
333
249
|
|
|
334
250
|
export async function sessions(options) {
|
|
335
|
-
const
|
|
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
|
|
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
|
|
374
|
-
if (
|
|
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 ||
|
|
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(`
|
|
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.
|
|
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;
|