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

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,375 @@
1
+ import { spawn } from 'node:child_process';
2
+ import {
3
+ closeSync,
4
+ existsSync,
5
+ openSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ readSync,
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';
13
+
14
+ const DEFAULT_PROVIDER = 'deepseek-gateway';
15
+ const REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
16
+ const TIME_WIDTH = 10;
17
+ const PROVIDER_WIDTH = 18;
18
+ const ID_WIDTH = 36;
19
+ const TITLE_WIDTH = 16;
20
+ const TABLE_INDENT = ' ';
21
+ const COLUMN_GAP = ' ';
22
+
23
+ function print(message = '') {
24
+ process.stdout.write(message);
25
+ }
26
+
27
+ function defaultCodexHome() {
28
+ return process.env.CODEX_HOME || join(process.env.USERPROFILE || process.env.HOME || process.cwd(), '.codex');
29
+ }
30
+
31
+ function readStart(file, bytes = 256 * 1024) {
32
+ const handle = openSync(file, 'r');
33
+ try {
34
+ const buffer = Buffer.alloc(bytes);
35
+ return buffer.subarray(0, readSync(handle, buffer, 0, bytes, 0)).toString('utf8');
36
+ } finally {
37
+ closeSync(handle);
38
+ }
39
+ }
40
+
41
+ function walkJsonl(dir, result = []) {
42
+ if (!existsSync(dir)) return result;
43
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
44
+ const path = join(dir, entry.name);
45
+ if (entry.isDirectory()) walkJsonl(path, result);
46
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) result.push(path);
47
+ }
48
+ return result;
49
+ }
50
+
51
+ function parseJsonLine(line) {
52
+ try {
53
+ return JSON.parse(line);
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function readSessionIndex(codexHome) {
60
+ const file = join(codexHome, 'session_index.jsonl');
61
+ const byId = new Map();
62
+ if (!existsSync(file)) return byId;
63
+ for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
64
+ if (!line.trim()) continue;
65
+ const row = parseJsonLine(line);
66
+ if (!row?.id) continue;
67
+ const prior = byId.get(row.id);
68
+ if (!prior || String(row.updated_at || '') > String(prior.updated_at || '')) byId.set(row.id, row);
69
+ }
70
+ return byId;
71
+ }
72
+
73
+ function textFromContent(content) {
74
+ if (!Array.isArray(content)) return '';
75
+ return content
76
+ .map((part) => part?.text || '')
77
+ .filter(Boolean)
78
+ .join(' ');
79
+ }
80
+
81
+ function firstUserPreview(lines) {
82
+ for (const line of lines.slice(1, 180)) {
83
+ const payload = parseJsonLine(line)?.payload;
84
+ if (payload?.type !== 'message' || payload.role !== 'user') continue;
85
+ const text = textFromContent(payload.content).trim().replace(/\s+/g, ' ');
86
+ if (!text || text.startsWith('<environment_context>') || text.startsWith('# AGENTS.md instructions')) continue;
87
+ return truncate(text, TITLE_WIDTH);
88
+ }
89
+ return '';
90
+ }
91
+
92
+ function truncate(text, max) {
93
+ return text.length > max ? `${text.slice(0, max - 1)}...` : text;
94
+ }
95
+
96
+ function pad(text, width) {
97
+ return truncate(String(text || ''), width).padEnd(width, ' ');
98
+ }
99
+
100
+ function formatTime(value) {
101
+ if (!value) return '';
102
+ return String(value).slice(0, 10);
103
+ }
104
+
105
+ function normalizePath(path) {
106
+ const resolved = resolve(path || '');
107
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
108
+ }
109
+
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
+ function isInsideProject(cwd, projectRoot) {
121
+ if (!cwd) return false;
122
+ const sessionCwd = normalizePath(cwd);
123
+ const root = normalizePath(projectRoot);
124
+ return sessionCwd === root || sessionCwd.startsWith(`${root}${process.platform === 'win32' ? '\\' : '/'}`);
125
+ }
126
+
127
+ function readSession(file, indexById) {
128
+ const lines = readStart(file).split(/\r?\n/).filter(Boolean);
129
+ const meta = parseJsonLine(lines[0])?.payload;
130
+ if (!meta?.id) return null;
131
+ const index = indexById.get(meta.id) || {};
132
+ return {
133
+ id: meta.id,
134
+ provider: meta.model_provider || '',
135
+ cwd: meta.cwd || '',
136
+ updatedAt: index.updated_at || meta.timestamp || '',
137
+ title: index.thread_name || firstUserPreview(lines) || '(untitled)',
138
+ };
139
+ }
140
+
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
+ function resolveSessionSelection(selection, sessionsList) {
167
+ if (/^\d+$/.test(selection)) {
168
+ const byIndex = sessionsList[Number(selection) - 1];
169
+ if (byIndex) return byIndex;
170
+ }
171
+ const matches = sessionsList.filter((session) => session.id.startsWith(selection));
172
+ return matches.length === 1 ? matches[0] : null;
173
+ }
174
+
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
+ function printSessions(sessionsList, options, context) {
199
+ const listed = sessionsList.slice(0, options.limit || 20);
200
+ print(`Codex sessions ${options.all ? `under ${context.codexHome}` : `for project ${context.projectRoot}`}\n`);
201
+ print(`Target: ${context.provider} / ${context.model} / ${context.reasoningEffort}\n\n`);
202
+ if (!listed.length) {
203
+ print('No matching sessions found.\n');
204
+ return;
205
+ }
206
+ 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`);
208
+ listed.forEach((session, index) => {
209
+ print(`${String(index + 1).padStart(2, ' ')} ${sessionRow(session)}\n`);
210
+ if (options.all) print(` cwd: ${session.cwd || '(unknown cwd)'}\n`);
211
+ print(` resume: ${resumeCommand(session.id, context)}\n\n`);
212
+ });
213
+ if (sessionsList.length > listed.length) print(`Showing ${listed.length} of ${sessionsList.length}. Use --limit ${sessionsList.length} or --all as needed.\n`);
214
+ }
215
+
216
+ function sessionHeader() {
217
+ return `${pad('Date', TIME_WIDTH)}${COLUMN_GAP}${pad('Provider', PROVIDER_WIDTH)}${COLUMN_GAP}${pad('Session ID', ID_WIDTH)}${COLUMN_GAP}Title`;
218
+ }
219
+
220
+ function sessionRow(session) {
221
+ return `${pad(formatTime(session.updatedAt), TIME_WIDTH)}${COLUMN_GAP}${pad(session.provider || '(unknown)', PROVIDER_WIDTH)}${COLUMN_GAP}${session.id}${COLUMN_GAP}${truncate(session.title, TITLE_WIDTH)}`;
222
+ }
223
+
224
+ function sessionRows(sessionsList) {
225
+ return sessionsList.map((session) => sessionRow(session));
226
+ }
227
+
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
+ 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`);
297
+ return null;
298
+ }
299
+
300
+ const stdin = process.stdin;
301
+ stdin.resume();
302
+ stdin.setRawMode(true);
303
+ openPickerScreen();
304
+ try {
305
+ let step = 'model';
306
+ 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];
311
+ step = 'reasoning';
312
+ } 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];
325
+ }
326
+ }
327
+ } finally {
328
+ stdin.setRawMode(false);
329
+ stdin.pause();
330
+ closePickerScreen();
331
+ }
332
+ }
333
+
334
+ 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
+ };
351
+ const seen = new Map();
352
+
353
+ for (const file of walkJsonl(sessionsDir)) {
354
+ const session = readSession(file, indexById);
355
+ if (!session || (!options.all && !isInsideProject(session.cwd, projectRoot))) continue;
356
+ const prior = seen.get(session.id);
357
+ if (!prior || session.updatedAt > prior.updatedAt) seen.set(session.id, session);
358
+ }
359
+
360
+ const allSessions = [...seen.values()].sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
361
+ if (options.exec) {
362
+ const session = resolveSessionSelection(options.exec, allSessions);
363
+ if (!session) throw new Error(`Session not found or ambiguous: ${options.exec}`);
364
+ await runCodexResume(session, context);
365
+ return;
366
+ }
367
+
368
+ if (options.print || !process.stdin.isTTY || !process.stdout.isTTY) {
369
+ printSessions(allSessions, options, context);
370
+ return;
371
+ }
372
+
373
+ const session = await chooseSessionFlow(allSessions, context, options.limit || 20);
374
+ if (session) await runCodexResume(session, context);
375
+ }
package/src/config.js CHANGED
@@ -32,6 +32,22 @@ export function loadConfig(env = process.env) {
32
32
  tavilyIncludeAnswer: parseBoolean(mergedEnv.TAVILY_INCLUDE_ANSWER, true),
33
33
  tavilyTopic: mergedEnv.TAVILY_TOPIC || '',
34
34
  tavilyTimeRange: mergedEnv.TAVILY_TIME_RANGE || '',
35
+ firecrawlWebFetchEnabled: parseBoolean(mergedEnv.FIRECRAWL_WEB_FETCH_ENABLED ?? mergedEnv.ENABLE_FIRECRAWL_WEB_FETCH, false),
36
+ firecrawlApiKey: mergedEnv.FIRECRAWL_API_KEY || '',
37
+ firecrawlBaseUrl: mergedEnv.FIRECRAWL_BASE_URL || 'https://api.firecrawl.dev',
38
+ firecrawlAutoScrapeTopResults: Number(mergedEnv.FIRECRAWL_AUTO_SCRAPE_TOP_RESULTS || 3),
39
+ firecrawlPageMaxChars: Number(mergedEnv.FIRECRAWL_PAGE_MAX_CHARS || 5000),
40
+ firecrawlResultMaxChars: Number(mergedEnv.FIRECRAWL_RESULT_MAX_CHARS || 12000),
41
+ firecrawlMaxLinks: Number(mergedEnv.FIRECRAWL_MAX_LINKS || 20),
42
+ firecrawlTimeoutMs: Number(mergedEnv.FIRECRAWL_TIMEOUT_MS || 30000),
43
+ firecrawlWaitForMs: Number(mergedEnv.FIRECRAWL_WAIT_FOR_MS || 0),
44
+ firecrawlFormats: mergedEnv.FIRECRAWL_FORMATS || 'markdown,links',
45
+ firecrawlOnlyMainContent: parseBoolean(mergedEnv.FIRECRAWL_ONLY_MAIN_CONTENT, true),
46
+ firecrawlRemoveBase64Images: parseBoolean(mergedEnv.FIRECRAWL_REMOVE_BASE64_IMAGES, true),
47
+ firecrawlIncludeLinks: parseBoolean(mergedEnv.FIRECRAWL_INCLUDE_LINKS, true),
48
+ firecrawlIncludeSummary: parseBoolean(mergedEnv.FIRECRAWL_INCLUDE_SUMMARY, false),
49
+ firecrawlMobile: mergedEnv.FIRECRAWL_MOBILE,
50
+ firecrawlAllowPrivateUrls: parseBoolean(mergedEnv.FIRECRAWL_ALLOW_PRIVATE_URLS, false),
35
51
  codexModelProvider: codexConfig.modelProvider,
36
52
  codexModel: codexConfig.model,
37
53
  codexReasoningEffort: mergedEnv.CODEX_REASONING_EFFORT || mergedEnv.MODEL_REASONING_EFFORT || codexConfig.modelReasoningEffort,