coding-tool-x 3.8.0 → 3.8.2

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +4 -6
  3. package/dist/web/assets/{Analytics-8dRv1aYW.js → Analytics-COEUXmd4.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-BQRXSICO.js → ConfigTemplates-DLLkS53H.js} +1 -1
  5. package/dist/web/assets/{Home-BtzWhKIa.js → Home-DCEyLzwX.js} +1 -1
  6. package/dist/web/assets/{PluginManager-BTa-Ygf6.js → PluginManager-Oe1ZHoMO.js} +1 -1
  7. package/dist/web/assets/{ProjectList-F_cB8ldA.js → ProjectList-CWjuAFof.js} +1 -1
  8. package/dist/web/assets/{SessionList-LOrbPQvu.js → SessionList-v3ehGg9U.js} +1 -1
  9. package/dist/web/assets/{SkillManager-BzI_t-yV.js → SkillManager-DUF_qP5S.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-Db9RGiz7.js → WorkspaceManager-CjAWBosO.js} +1 -1
  11. package/dist/web/assets/index-D5hyHPj7.js +2 -0
  12. package/dist/web/index.html +1 -1
  13. package/docs/superpowers/plans/2026-08-10-codex-conversion-compatibility.md +251 -0
  14. package/docs/superpowers/plans/2026-08-11-windows-command-window-hiding.md +90 -0
  15. package/docs/superpowers/specs/2026-08-10-codex-conversion-compatibility-design.md +61 -0
  16. package/docs/superpowers/specs/2026-08-11-windows-command-window-hiding-design.md +44 -0
  17. package/package.json +2 -2
  18. package/src/commands/update.js +1 -1
  19. package/src/config/paths.js +1 -0
  20. package/src/server/api/codex-sessions.js +14 -14
  21. package/src/server/api/gemini-sessions.js +52 -14
  22. package/src/server/api/omp-projects.js +2 -2
  23. package/src/server/api/omp-sessions.js +61 -23
  24. package/src/server/api/sessions.js +44 -2
  25. package/src/server/opencode-proxy-server.js +55 -1037
  26. package/src/server/services/claude-wire.js +694 -0
  27. package/src/server/services/codex-sessions.js +41 -233
  28. package/src/server/services/codex-wire.js +219 -0
  29. package/src/server/services/dashboard-snapshot-worker.js +5 -5
  30. package/src/server/services/gemini-sessions.js +67 -157
  31. package/src/server/services/gemini-wire.js +759 -0
  32. package/src/server/services/model-detector.js +49 -131
  33. package/src/server/services/omp-channels.js +23 -4
  34. package/src/server/services/omp-gateway.js +29 -1
  35. package/src/server/services/omp-sessions.js +102 -105
  36. package/src/server/services/omp-settings-manager.js +27 -3
  37. package/src/server/services/opencode-gateway-adapters.js +22 -1147
  38. package/src/server/services/opencode-normalization.js +546 -0
  39. package/src/server/services/opencode-sessions.js +339 -546
  40. package/src/server/services/session-converter.js +4 -4
  41. package/src/server/services/session-history-adapters/claude.js +235 -0
  42. package/src/server/services/session-history-adapters/codex.js +158 -0
  43. package/src/server/services/session-history-adapters/gemini.js +292 -0
  44. package/src/server/services/session-history-adapters/index.js +20 -0
  45. package/src/server/services/session-history-adapters/omp.js +256 -0
  46. package/src/server/services/session-history-index.js +930 -0
  47. package/src/server/services/session-history-worker.js +99 -0
  48. package/src/server/services/session-snapshot-worker.js +7 -7
  49. package/src/server/services/sessions.js +167 -775
  50. package/src/server/services/speed-test.js +85 -303
  51. package/src/server/services/sqlite-connection.js +78 -0
  52. package/src/server/services/workspace-service.js +3 -3
  53. package/dist/web/assets/index-DIWJmyL6.js +0 -2
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const { getCodexDir } = require('./codex-config');
4
4
  const { parseSession, parseSessionMeta, extractSessionMeta, readJSONL } = require('./codex-parser');
5
5
  const { globalCache, CacheKeys } = require('./enhanced-cache');
6
+ const { listProjects: idxListProjects, listSessions: idxListSessions, getSessionStatus: idxGetSessionStatus, getRecentSessions: idxGetRecent, searchSessions: idxSearch } = require('./session-history-index');
6
7
 
7
8
  const COUNTS_CACHE_TTL_MS = 30 * 1000;
8
9
  const SCAN_FILES_CACHE_TTL_MS = 15 * 1000;
@@ -227,78 +228,15 @@ function normalizeSession(codexSession) {
227
228
  * 聚合项目列表
228
229
  * @returns {Array} 项目对象数组
229
230
  */
230
- function getProjects(options = {}) {
231
- if (options.force) {
232
- invalidateCodexSessionCaches();
233
- } else {
234
- const cached = globalCache.get(CODEX_PROJECTS_CACHE_KEY);
235
- if (cached) {
236
- return cached;
237
- }
238
- }
239
-
240
- const sessions = getAllSessions();
241
- const projectMap = new Map();
242
-
243
- sessions.forEach(session => {
244
- const meta = session.meta;
245
-
246
- // 优先使用 Git 仓库名,否则使用 cwd 的最后一级目录
247
- const projectName = extractCodexProjectNameFromMeta(meta);
248
- const projectPath = (typeof meta.cwd === 'string' && meta.cwd.trim()) ? meta.cwd.trim() : projectName;
249
- if (!projectName) return;
250
-
251
- if (!projectMap.has(projectName)) {
252
- projectMap.set(projectName, {
253
- name: projectName,
254
- displayName: projectName,
255
- fullPath: projectPath,
256
- path: projectPath,
257
- gitRepo: meta.git?.repositoryUrl,
258
- branch: meta.git?.branch,
259
- sessionCount: 0,
260
- lastUsed: null,
261
- source: 'codex'
262
- });
263
- }
264
-
265
- const project = projectMap.get(projectName);
266
- project.sessionCount++;
267
-
268
- // 更新最后活动时间
269
- const sessionTime = session.mtimeMs || new Date(session.meta.timestamp || 0).getTime() || 0;
270
- if (!project.lastUsed || sessionTime > project.lastUsed) {
271
- project.lastUsed = sessionTime;
272
- }
273
- });
274
-
275
- // 获取保存的排序
231
+ async function getProjects(options = {}) {
232
+ const projects = await idxListProjects('codex', options);
276
233
  const savedOrder = getProjectOrder();
277
- const projects = Array.from(projectMap.values());
278
-
279
- // 应用保存的排序
280
- if (savedOrder.length > 0) {
281
- const ordered = [];
282
- const projectsMap = new Map(projects.map(p => [p.name, p]));
283
-
284
- // 按保存的顺序添加项目
285
- for (const projectName of savedOrder) {
286
- if (projectsMap.has(projectName)) {
287
- ordered.push(projectsMap.get(projectName));
288
- projectsMap.delete(projectName);
289
- }
290
- }
291
-
292
- // 添加剩余的新项目(不在保存顺序中的)
293
- ordered.push(...projectsMap.values());
294
- globalCache.set(CODEX_PROJECTS_CACHE_KEY, ordered, PROJECTS_CACHE_TTL_MS);
295
- return ordered;
296
- }
234
+ if (savedOrder.length === 0) return projects;
297
235
 
298
- // 默认按最后活动时间排序
299
- const sorted = projects.sort((a, b) => (b.lastUsed || 0) - (a.lastUsed || 0));
300
- globalCache.set(CODEX_PROJECTS_CACHE_KEY, sorted, PROJECTS_CACHE_TTL_MS);
301
- return sorted;
236
+ const byName = new Map(projects.map(project => [project.name, project]));
237
+ const ordered = savedOrder.flatMap(name => byName.has(name) ? [byName.get(name)] : []);
238
+ savedOrder.forEach(name => byName.delete(name));
239
+ return [...ordered, ...byName.values()];
302
240
  }
303
241
 
304
242
  /**
@@ -306,71 +244,23 @@ function getProjects(options = {}) {
306
244
  * @param {string} projectName - 项目名称
307
245
  * @returns {Array} 归一化的会话数组
308
246
  */
309
- function getSessionsByProject(projectName, options = {}) {
310
- const cacheKey = getCodexSessionsCacheKey(projectName);
311
- if (options.force) {
312
- invalidateCodexSessionCaches({ projectName });
313
- } else {
314
- const cached = globalCache.get(cacheKey);
315
- if (cached) {
316
- return cached;
317
- }
318
- }
319
-
320
- const sessions = getAllSessions();
321
-
322
- // 获取 fork 关系
323
- const { getForkRelations } = require('./sessions');
324
- const forkRelations = getForkRelations();
325
-
326
- // 获取保存的排序
247
+ async function getSessionsByProject(projectName, options = {}) {
248
+ const indexed = await idxListSessions('codex', projectName, options);
249
+ const forkRelations = require('./sessions').getForkRelations();
250
+ const aliases = require('./alias').loadAliases();
251
+ const sessions = indexed.map(session => ({
252
+ ...session,
253
+ mtime: new Date(session.mtime).toISOString(),
254
+ forkedFrom: forkRelations[session.sessionId] || null,
255
+ alias: aliases[session.sessionId] || null
256
+ }));
327
257
  const savedOrder = getSessionOrder(projectName);
258
+ if (savedOrder.length === 0) return sessions;
328
259
 
329
- // 过滤并归一化会话
330
- const filteredSessions = sessions
331
- .filter(session => {
332
- const sessionProjectName = extractCodexProjectNameFromMeta(session.meta);
333
- return sessionProjectName === projectName;
334
- })
335
- .map(session => {
336
- const normalized = normalizeSession(session);
337
- // 添加 fork 关系
338
- normalized.forkedFrom = forkRelations[normalized.sessionId] || null;
339
- return normalized;
340
- });
341
-
342
- // 应用保存的排序
343
- let orderedSessions = filteredSessions;
344
- if (savedOrder.length > 0) {
345
- const orderedFromSaved = [];
346
- const sessionMap = new Map(filteredSessions.map(s => [s.sessionId, s]));
347
-
348
- for (const sessionId of savedOrder) {
349
- const session = sessionMap.get(sessionId);
350
- if (session) {
351
- orderedFromSaved.push(session);
352
- sessionMap.delete(sessionId);
353
- }
354
- }
355
-
356
- const newSessions = [...sessionMap.values()];
357
- newSessions.sort((a, b) => {
358
- return new Date(b.mtime).getTime() - new Date(a.mtime).getTime();
359
- });
360
-
361
- // 新会话在前,旧会话在后(按保存顺序)
362
- orderedSessions = [...newSessions, ...orderedFromSaved];
363
- } else {
364
- // 默认按时间倒序
365
- orderedSessions.sort((a, b) => {
366
- return new Date(b.mtime).getTime() - new Date(a.mtime).getTime();
367
- });
368
- }
369
-
370
- globalCache.set(cacheKey, orderedSessions, PROJECT_SESSIONS_CACHE_TTL_MS);
371
- codexSessionCacheKeys.add(cacheKey);
372
-
373
- return orderedSessions;
260
+ const byId = new Map(sessions.map(session => [session.sessionId, session]));
261
+ const ordered = savedOrder.flatMap(id => byId.has(id) ? [byId.get(id)] : []);
262
+ savedOrder.forEach(id => byId.delete(id));
263
+ return [...byId.values(), ...ordered];
374
264
  }
375
265
 
376
266
  /**
@@ -378,23 +268,12 @@ function getSessionsByProject(projectName, options = {}) {
378
268
  * @param {string} sessionId - 会话 ID
379
269
  * @returns {Object|null} 归一化的会话对象
380
270
  */
381
- function getSessionById(sessionId) {
382
- const file = findSessionFileById(sessionId);
383
-
384
- if (!file) {
385
- return null;
386
- }
387
-
388
- const session = parseSession(file.filePath);
389
- if (!session) {
390
- return null;
391
- }
392
-
393
- return {
394
- ...normalizeSession(session),
395
- messages: session.messages, // 包含完整消息
396
- filePath: file.filePath
397
- };
271
+ async function getSessionById(sessionId) {
272
+ const status = await idxGetSessionStatus('codex', sessionId);
273
+ if (!status) return null;
274
+ const session = parseSession(status.filePath);
275
+ if (!session) return null;
276
+ return { ...normalizeSession(session), messages: session.messages, filePath: status.filePath };
398
277
  }
399
278
 
400
279
  /**
@@ -402,56 +281,8 @@ function getSessionById(sessionId) {
402
281
  * @param {string} keyword - 搜索关键词
403
282
  * @returns {Array} 搜索结果
404
283
  */
405
- function searchSessions(keyword) {
406
- const files = scanSessionFiles();
407
- const results = [];
408
-
409
- files.forEach(file => {
410
- // 使用完整解析获取消息内容
411
- const session = parseSession(file.filePath);
412
-
413
- if (!session || !session.messages || !Array.isArray(session.messages)) {
414
- return;
415
- }
416
-
417
- session.messages.forEach((message, index) => {
418
- if (message.role !== 'user' && message.role !== 'assistant') {
419
- return;
420
- }
421
-
422
- const content = (message.content || '').toLowerCase();
423
- const keywordLower = keyword.toLowerCase();
424
-
425
- if (content.includes(keywordLower)) {
426
- // 提取上下文
427
- const startIndex = Math.max(0, content.indexOf(keywordLower) - 50);
428
- const endIndex = Math.min(content.length, content.indexOf(keywordLower) + keyword.length + 50);
429
- const context = content.substring(startIndex, endIndex);
430
-
431
- // 确定项目名
432
- let projectName;
433
- if (session.meta?.git?.repositoryUrl) {
434
- projectName = session.meta.git.repositoryUrl.split('/').pop().replace('.git', '');
435
- } else if (session.meta?.cwd) {
436
- projectName = path.basename(session.meta.cwd);
437
- } else {
438
- projectName = 'Unknown';
439
- }
440
-
441
- results.push({
442
- sessionId: file.sessionId,
443
- projectName,
444
- messageIndex: index,
445
- role: message.role,
446
- context: (startIndex > 0 ? '...' : '') + context + (endIndex < content.length ? '...' : ''),
447
- timestamp: message.timestamp,
448
- source: 'codex'
449
- });
450
- }
451
- });
452
- });
453
-
454
- return results;
284
+ async function searchSessions(keyword) {
285
+ return idxSearch('codex', keyword);
455
286
  }
456
287
 
457
288
  /**
@@ -545,39 +376,16 @@ function deleteProject(projectName) {
545
376
  * @param {number} limit - 返回数量限制,默认 5
546
377
  * @returns {Array} 最近会话数组
547
378
  */
548
- function getRecentSessions(limit = 5) {
549
- const sessions = getAllSessions();
550
-
551
- // 获取 fork 关系和别名
552
- const { getForkRelations } = require('./sessions');
553
- const { loadAliases } = require('./alias');
554
- const forkRelations = getForkRelations();
555
- const aliases = loadAliases();
556
-
557
- // 归一化所有会话
558
- const allNormalizedSessions = sessions.map(session => {
559
- const normalized = normalizeSession(session);
560
-
561
- // 添加项目信息
562
- const projectName = extractCodexProjectNameFromMeta(session.meta) || 'Unknown';
563
- const projectPath = (typeof session.meta.cwd === 'string' && session.meta.cwd.trim())
564
- ? session.meta.cwd
565
- : projectName;
566
-
567
- return {
568
- ...normalized,
569
- forkedFrom: forkRelations[normalized.sessionId] || null,
570
- alias: aliases[normalized.sessionId] || null,
571
- projectName: projectName,
572
- projectDisplayName: projectName,
573
- projectFullPath: projectPath
574
- };
575
- });
576
-
577
- // 按 mtime 倒序排序,取前 N 个
578
- return allNormalizedSessions
579
- .sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime())
580
- .slice(0, limit);
379
+ async function getRecentSessions(limit = 5) {
380
+ const indexed = await idxGetRecent('codex', limit);
381
+ const forkRelations = require('./sessions').getForkRelations();
382
+ const aliases = require('./alias').loadAliases();
383
+ return indexed.map(session => ({
384
+ ...session,
385
+ mtime: new Date(session.mtime).toISOString(),
386
+ forkedFrom: forkRelations[session.sessionId] || null,
387
+ alias: aliases[session.sessionId] || null
388
+ }));
581
389
  }
582
390
 
583
391
  /**
@@ -0,0 +1,219 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const os = require('os');
5
+
6
+ const CODEX_CLIENT_VERSION = '0.144.1';
7
+ const CODEX_ORIGINATOR = 'codex_exec';
8
+ const CODEX_RESERVED_METADATA_KEYS = new Set([
9
+ 'x-codex-installation-id',
10
+ 'x-codex-window-id',
11
+ 'x-codex-turn-metadata',
12
+ 'session_id',
13
+ 'thread_id',
14
+ 'turn_id'
15
+ ]);
16
+ const CODEX_UNSUPPORTED_BODY_FIELDS = [
17
+ 'max_output_tokens',
18
+ 'max_completion_tokens',
19
+ 'temperature',
20
+ 'top_p',
21
+ 'top_k',
22
+ 'min_p',
23
+ 'presence_penalty',
24
+ 'frequency_penalty',
25
+ 'repetition_penalty',
26
+ 'stop',
27
+ 'service_tier',
28
+ 'user',
29
+ 'previous_response_id',
30
+ 'prompt_cache_retention',
31
+ 'safety_identifier'
32
+ ];
33
+
34
+ let installationId = '';
35
+
36
+ function cloneJsonCompatible(value) {
37
+ try {
38
+ return JSON.parse(JSON.stringify(value));
39
+ } catch {
40
+ return value;
41
+ }
42
+ }
43
+
44
+ function createId() {
45
+ return typeof crypto.randomUUID === 'function'
46
+ ? crypto.randomUUID()
47
+ : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 14)}`;
48
+ }
49
+
50
+ function getInstallationId() {
51
+ if (!installationId) installationId = createId();
52
+ return installationId;
53
+ }
54
+
55
+ function normalizeId(value, fallbackFactory = createId) {
56
+ const normalized = typeof value === 'string' ? value.trim() : '';
57
+ return normalized || fallbackFactory();
58
+ }
59
+
60
+ function normalizeCodexResponsesInput(inputValue) {
61
+ if (typeof inputValue === 'string') {
62
+ return [
63
+ {
64
+ type: 'message',
65
+ role: 'user',
66
+ content: [{ type: 'input_text', text: inputValue }]
67
+ }
68
+ ];
69
+ }
70
+
71
+ if (!Array.isArray(inputValue)) return undefined;
72
+ return inputValue.map(item => {
73
+ if (!item || typeof item !== 'object') return item;
74
+ const clonedItem = cloneJsonCompatible(item);
75
+ if (String(clonedItem?.role || '').trim().toLowerCase() === 'system') {
76
+ clonedItem.role = 'developer';
77
+ }
78
+ return clonedItem;
79
+ });
80
+ }
81
+
82
+ function normalizeInclude(value) {
83
+ const include = Array.isArray(value)
84
+ ? value.filter(item => typeof item === 'string' && item.trim())
85
+ : [];
86
+ if (!include.includes('reasoning.encrypted_content')) {
87
+ include.push('reasoning.encrypted_content');
88
+ }
89
+ return [...new Set(include)];
90
+ }
91
+
92
+ function normalizeClientMetadata(value) {
93
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
94
+ return Object.fromEntries(
95
+ Object.entries(value)
96
+ .filter(([key, item]) => !CODEX_RESERVED_METADATA_KEYS.has(key) && typeof item === 'string')
97
+ .map(([key, item]) => [key, item])
98
+ );
99
+ }
100
+
101
+ function resolveUserAgent(value) {
102
+ if (typeof value === 'string' && value.trim()) return value.trim();
103
+ const terminal = String(process.env.TERM_PROGRAM || process.env.TERM || 'Terminal').trim() || 'Terminal';
104
+ const platformName = process.platform === 'darwin' ? 'Mac OS' : process.platform;
105
+ return `${CODEX_ORIGINATOR}/${CODEX_CLIENT_VERSION} (${platformName} ${os.release()}; ${process.arch}) ${terminal} (${CODEX_ORIGINATOR}; ${CODEX_CLIENT_VERSION})`;
106
+ }
107
+
108
+ function buildTurnMetadata({ installationId: currentInstallationId, sessionId, threadId, turnId, windowId }) {
109
+ return JSON.stringify({
110
+ installation_id: currentInstallationId,
111
+ session_id: sessionId,
112
+ thread_id: threadId,
113
+ turn_id: turnId,
114
+ window_id: windowId,
115
+ request_kind: 'turn'
116
+ });
117
+ }
118
+
119
+ function createCodexRequest(payload = {}, options = {}) {
120
+ const source = payload && typeof payload === 'object' && !Array.isArray(payload)
121
+ ? payload
122
+ : {};
123
+ const body = cloneJsonCompatible(source);
124
+ const fallbackModel = typeof options.fallbackModel === 'string' ? options.fallbackModel.trim() : '';
125
+ const requestedSessionId = options.sessionId || body.prompt_cache_key;
126
+ if (body.model === undefined && fallbackModel) body.model = fallbackModel;
127
+
128
+ const sessionId = normalizeId(requestedSessionId, createId);
129
+ const threadId = normalizeId(options.threadId, () => sessionId);
130
+ const windowId = normalizeId(options.windowId, () => `${sessionId}:0`);
131
+ const turnId = normalizeId(options.turnId, createId);
132
+ const currentInstallationId = normalizeId(options.installationId, getInstallationId);
133
+ const normalizedInput = normalizeCodexResponsesInput(body.input);
134
+ if (normalizedInput !== undefined) body.input = normalizedInput;
135
+ body.stream = true;
136
+ if (body.parallel_tool_calls === undefined) body.parallel_tool_calls = true;
137
+ if (typeof body.instructions !== 'string') body.instructions = '';
138
+ body.store = false;
139
+ body.include = normalizeInclude(body.include);
140
+ const reasoningEffort = typeof body.reasoning_effort === 'string'
141
+ ? body.reasoning_effort.trim().toLowerCase()
142
+ : '';
143
+ if (reasoningEffort && body.reasoning === undefined && ['minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'none'].includes(reasoningEffort)) {
144
+ body.reasoning = { effort: reasoningEffort };
145
+ }
146
+ delete body.reasoning_effort;
147
+ body.prompt_cache_key = normalizeId(body.prompt_cache_key, () => sessionId);
148
+
149
+ const callerMetadata = normalizeClientMetadata(body.client_metadata);
150
+ const turnMetadata = buildTurnMetadata({
151
+ installationId: currentInstallationId,
152
+ sessionId,
153
+ threadId,
154
+ turnId,
155
+ windowId
156
+ });
157
+ body.client_metadata = {
158
+ ...callerMetadata,
159
+ 'x-codex-installation-id': currentInstallationId,
160
+ session_id: sessionId,
161
+ thread_id: threadId,
162
+ 'x-codex-window-id': windowId,
163
+ turn_id: turnId,
164
+ 'x-codex-turn-metadata': turnMetadata
165
+ };
166
+
167
+ CODEX_UNSUPPORTED_BODY_FIELDS.forEach(field => delete body[field]);
168
+
169
+ const headers = {
170
+ authorization: `Bearer ${String(options.apiKey || '')}`,
171
+ accept: 'text/event-stream',
172
+ 'content-type': 'application/json',
173
+ originator: typeof options.originator === 'string' && options.originator.trim()
174
+ ? options.originator.trim()
175
+ : CODEX_ORIGINATOR,
176
+ 'user-agent': resolveUserAgent(options.userAgent),
177
+ 'session-id': sessionId,
178
+ 'thread-id': threadId,
179
+ 'x-client-request-id': sessionId,
180
+ 'x-codex-window-id': windowId,
181
+ 'x-codex-turn-metadata': turnMetadata,
182
+ 'x-codex-beta-features': 'remote_compaction_v2'
183
+ };
184
+
185
+ return {
186
+ body,
187
+ headers,
188
+ model: String(body.model || fallbackModel || '').trim()
189
+ };
190
+ }
191
+
192
+ function buildCodexTargetUrl(baseUrl = '') {
193
+ let targetUrl;
194
+ try {
195
+ targetUrl = new URL(String(baseUrl || '').trim());
196
+ } catch {
197
+ return '';
198
+ }
199
+
200
+ let pathname = targetUrl.pathname.replace(/\/+$/, '');
201
+ if (!pathname || pathname === '/') {
202
+ pathname = '/responses';
203
+ } else if (pathname.endsWith('/responses') || pathname.endsWith('/v1/responses')) {
204
+ // Already an endpoint.
205
+ } else {
206
+ pathname = `${pathname}/responses`;
207
+ }
208
+
209
+ targetUrl.pathname = pathname;
210
+ return targetUrl.toString();
211
+ }
212
+
213
+ module.exports = {
214
+ CODEX_CLIENT_VERSION,
215
+ CODEX_ORIGINATOR,
216
+ buildCodexTargetUrl,
217
+ createCodexRequest,
218
+ normalizeCodexResponsesInput
219
+ };
@@ -32,12 +32,12 @@ async function buildProjectsPayload(source, config = {}, options = {}) {
32
32
  }
33
33
  case 'codex': {
34
34
  const { getProjects } = require('./codex-sessions');
35
- const projects = getProjects({ force: options.force === true });
35
+ const projects = await getProjects({ force: options.force === true });
36
36
  return { projects, currentProject: projects[0] ? projects[0].name : null };
37
37
  }
38
38
  case 'gemini': {
39
39
  const { getProjects } = require('./gemini-sessions');
40
- const projects = getProjects({ force: options.force === true });
40
+ const projects = await getProjects({ force: options.force === true });
41
41
  return { projects, currentProject: projects[0] ? projects[0].name : null };
42
42
  }
43
43
  case 'opencode': {
@@ -47,7 +47,7 @@ async function buildProjectsPayload(source, config = {}, options = {}) {
47
47
  }
48
48
  case 'omp': {
49
49
  const { getProjects } = require('./omp-sessions');
50
- const projects = getProjects({ force: options.force === true });
50
+ const projects = await getProjects({ force: options.force === true });
51
51
  return { projects, currentProject: projects[0] ? projects[0].name : null };
52
52
  }
53
53
  default:
@@ -55,10 +55,10 @@ async function buildProjectsPayload(source, config = {}, options = {}) {
55
55
  }
56
56
  }
57
57
 
58
- function buildCountsPayload(source, config = {}, options = {}) {
58
+ async function buildCountsPayload(source, config = {}, options = {}) {
59
59
  switch (source) {
60
60
  case 'claude':
61
- return require('./sessions').getProjectAndSessionCounts(config, { force: options.force === true });
61
+ return require('./sessions').getProjectAndSessionCounts(config);
62
62
  case 'codex':
63
63
  return require('./codex-sessions').getProjectAndSessionCounts({ force: options.force === true });
64
64
  case 'gemini':