codexmate 0.0.25 → 0.0.27

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 (35) hide show
  1. package/README.md +11 -3
  2. package/README.zh.md +10 -2
  3. package/cli/builtin-proxy.js +315 -95
  4. package/cli/openai-bridge.js +99 -5
  5. package/cli/session-convert-args.js +65 -0
  6. package/cli/session-convert-io.js +82 -0
  7. package/cli/session-convert.js +43 -0
  8. package/cli.js +547 -32
  9. package/package.json +74 -74
  10. package/web-ui/app.js +24 -2
  11. package/web-ui/logic.session-convert.mjs +70 -0
  12. package/web-ui/logic.sessions.mjs +151 -0
  13. package/web-ui/modules/app.computed.dashboard.mjs +44 -1
  14. package/web-ui/modules/app.computed.session.mjs +336 -12
  15. package/web-ui/modules/app.methods.claude-config.mjs +11 -1
  16. package/web-ui/modules/app.methods.codex-config.mjs +76 -0
  17. package/web-ui/modules/app.methods.navigation.mjs +51 -3
  18. package/web-ui/modules/app.methods.session-actions.mjs +55 -3
  19. package/web-ui/modules/app.methods.session-browser.mjs +270 -3
  20. package/web-ui/modules/app.methods.session-timeline.mjs +34 -3
  21. package/web-ui/modules/app.methods.session-trash.mjs +16 -1
  22. package/web-ui/modules/app.methods.startup-claude.mjs +234 -125
  23. package/web-ui/modules/i18n.dict.mjs +76 -0
  24. package/web-ui/partials/index/panel-config-claude.html +12 -4
  25. package/web-ui/partials/index/panel-sessions.html +33 -10
  26. package/web-ui/partials/index/panel-settings.html +16 -0
  27. package/web-ui/partials/index/panel-usage.html +95 -85
  28. package/web-ui/session-helpers.mjs +3 -0
  29. package/web-ui/styles/base-theme.css +29 -25
  30. package/web-ui/styles/layout-shell.css +1 -1
  31. package/web-ui/styles/navigation-panels.css +9 -9
  32. package/web-ui/styles/sessions-list.css +17 -0
  33. package/web-ui/styles/sessions-toolbar-trash.css +62 -0
  34. package/web-ui/styles/sessions-usage.css +211 -83
  35. package/web-ui/styles/settings-panel.css +19 -0
package/package.json CHANGED
@@ -1,74 +1,74 @@
1
- {
2
- "name": "codexmate",
3
- "version": "0.0.25",
4
- "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
- "main": "cli.js",
6
- "bin": {
7
- "codexmate": "./cli.js"
8
- },
9
- "files": [
10
- "cli.js",
11
- "cli/",
12
- "plugins/",
13
- "web-ui.html",
14
- "lib/",
15
- "web-ui/",
16
- "doc/",
17
- "README.md",
18
- "README.zh.md",
19
- "LICENSE"
20
- ],
21
- "exports": {
22
- ".": "./cli.js",
23
- "./lib/*": "./lib/*.js",
24
- "./web-ui/*": "./web-ui/*",
25
- "./res/*": "./web-ui/res/*"
26
- },
27
- "scripts": {
28
- "dev": "node cli.js run",
29
- "start": "node cli.js",
30
- "reset": "node tools/dev/reset-main.js",
31
- "release:npm": "node tools/release/publish-npm.js",
32
- "docs:dev": "node ./node_modules/vitepress/dist/node/cli.js dev site",
33
- "docs:build": "node ./node_modules/vitepress/dist/node/cli.js build site",
34
- "docs:preview": "node ./node_modules/vitepress/dist/node/cli.js preview site",
35
- "ci:install": "node tools/ci/run-check.js install",
36
- "ci:lint": "node tools/ci/run-check.js lint",
37
- "ci:test": "node tools/ci/run-check.js test",
38
- "lint": "node tools/dev/lint.js",
39
- "test": "npm run test:unit && npm run test:e2e",
40
- "test:ci": "node tools/ci/run-check.js all",
41
- "test:unit": "node tests/unit/run.mjs",
42
- "test:e2e": "node tests/e2e/run.js",
43
- "pretest": "node tools/ci/ensure-test-deps.js"
44
- },
45
- "dependencies": {
46
- "@iarna/toml": "^2.2.5",
47
- "json5": "^2.2.3",
48
- "yauzl": "^3.2.1",
49
- "zip-lib": "^1.2.1"
50
- },
51
- "engines": {
52
- "node": ">=14"
53
- },
54
- "keywords": [
55
- "codex",
56
- "claude",
57
- "claude-code",
58
- "openclaw",
59
- "config",
60
- "session",
61
- "task-orchestration",
62
- "workflow",
63
- "web-ui",
64
- "provider",
65
- "ai",
66
- "llm",
67
- "cli"
68
- ],
69
- "author": "ymkiux",
70
- "license": "Apache-2.0",
71
- "devDependencies": {
72
- "vitepress": "^1.6.4"
73
- }
74
- }
1
+ {
2
+ "name": "codexmate",
3
+ "version": "0.0.27",
4
+ "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
+ "main": "cli.js",
6
+ "bin": {
7
+ "codexmate": "./cli.js"
8
+ },
9
+ "files": [
10
+ "cli.js",
11
+ "cli/",
12
+ "plugins/",
13
+ "web-ui.html",
14
+ "lib/",
15
+ "web-ui/",
16
+ "doc/",
17
+ "README.md",
18
+ "README.zh.md",
19
+ "LICENSE"
20
+ ],
21
+ "exports": {
22
+ ".": "./cli.js",
23
+ "./lib/*": "./lib/*.js",
24
+ "./web-ui/*": "./web-ui/*",
25
+ "./res/*": "./web-ui/res/*"
26
+ },
27
+ "scripts": {
28
+ "dev": "node cli.js run",
29
+ "start": "node cli.js",
30
+ "reset": "node tools/dev/reset-main.js",
31
+ "release:npm": "node tools/release/publish-npm.js",
32
+ "docs:dev": "node ./node_modules/vitepress/dist/node/cli.js dev site",
33
+ "docs:build": "node ./node_modules/vitepress/dist/node/cli.js build site",
34
+ "docs:preview": "node ./node_modules/vitepress/dist/node/cli.js preview site",
35
+ "ci:install": "node tools/ci/run-check.js install",
36
+ "ci:lint": "node tools/ci/run-check.js lint",
37
+ "ci:test": "node tools/ci/run-check.js test",
38
+ "lint": "node tools/dev/lint.js",
39
+ "test": "npm run test:unit && npm run test:e2e",
40
+ "test:ci": "node tools/ci/run-check.js all",
41
+ "test:unit": "node tests/unit/run.mjs",
42
+ "test:e2e": "node tests/e2e/run.js",
43
+ "pretest": "node tools/ci/ensure-test-deps.js"
44
+ },
45
+ "dependencies": {
46
+ "@iarna/toml": "^2.2.5",
47
+ "json5": "^2.2.3",
48
+ "yauzl": "^3.2.1",
49
+ "zip-lib": "^1.2.1"
50
+ },
51
+ "engines": {
52
+ "node": ">=14"
53
+ },
54
+ "keywords": [
55
+ "codex",
56
+ "claude",
57
+ "claude-code",
58
+ "openclaw",
59
+ "config",
60
+ "session",
61
+ "task-orchestration",
62
+ "workflow",
63
+ "web-ui",
64
+ "provider",
65
+ "ai",
66
+ "llm",
67
+ "cli"
68
+ ],
69
+ "author": "ymkiux",
70
+ "license": "Apache-2.0",
71
+ "devDependencies": {
72
+ "vitepress": "^1.6.4"
73
+ }
74
+ }
package/web-ui/app.js CHANGED
@@ -112,7 +112,6 @@ document.addEventListener('DOMContentLoaded', () => {
112
112
  _pendingCodexApplyOptions: null,
113
113
  agentsContent: '',
114
114
  agentsPath: '',
115
- agentsPath: '',
116
115
  agentsExists: false,
117
116
  agentsLineEnding: '\n',
118
117
  agentsLoading: false,
@@ -158,8 +157,10 @@ document.addEventListener('DOMContentLoaded', () => {
158
157
  ticket: 0
159
158
  },
160
159
  sessionsViewMode: 'browser',
161
- sessionsUsageTimeRange: '7d',
160
+ sessionsUsageTimeRange: (function () { try { const saved = localStorage.getItem('sessionsUsageTimeRange'); if (saved === '7d' || saved === '30d' || saved === 'all') return saved; } catch (_) {} return '7d'; })(),
162
161
  sessionsUsageList: [],
162
+ sessionsUsageCompareEnabled: false,
163
+ sessionsUsageSelectedDayKey: '',
163
164
  sessionsUsageLoadedOnce: false,
164
165
  sessionsUsageLoadedLimit: 0,
165
166
  sessionsUsageLoading: false,
@@ -172,6 +173,7 @@ document.addEventListener('DOMContentLoaded', () => {
172
173
  sessionQuery: '',
173
174
  sessionRoleFilter: 'all',
174
175
  sessionTimePreset: 'all',
176
+ sessionSortMode: 'time',
175
177
  sessionResumeWithYolo: true,
176
178
  sessionPathOptions: [],
177
179
  sessionPathOptionsLoading: false,
@@ -194,6 +196,7 @@ document.addEventListener('DOMContentLoaded', () => {
194
196
  gemini: 0
195
197
  },
196
198
  sessionExporting: {},
199
+ sessionConverting: {},
197
200
  sessionCloning: {},
198
201
  sessionDeleting: {},
199
202
  activeSession: null,
@@ -261,6 +264,7 @@ document.addEventListener('DOMContentLoaded', () => {
261
264
  newModelName: '',
262
265
  currentClaudeConfig: '',
263
266
  currentClaudeModel: '',
267
+ claudeCustomModelDraft: '',
264
268
  editingConfig: { name: '', apiKey: '', baseUrl: '', model: '' },
265
269
  claudeConfigs: {
266
270
  '智谱GLM': {
@@ -358,6 +362,7 @@ document.addEventListener('DOMContentLoaded', () => {
358
362
  sessionTrashRestoring: {},
359
363
  sessionTrashPurging: {},
360
364
  sessionTrashClearing: false,
365
+ sessionTrashRetentionDays: 30,
361
366
  claudeImportLoading: false,
362
367
  codexImportLoading: false,
363
368
  codexAuthProfiles: [],
@@ -474,6 +479,7 @@ document.addEventListener('DOMContentLoaded', () => {
474
479
  this.restoreSessionPinnedMap();
475
480
  this.shareCommandPrefix = this.normalizeShareCommandPrefix(localStorage.getItem('codexmateShareCommandPrefix'));
476
481
  this.sessionTrashEnabled = this.normalizeSessionTrashEnabled(localStorage.getItem('codexmateSessionTrashEnabled'));
482
+ this.sessionTrashRetentionDays = this.normalizeSessionTrashRetentionDays(localStorage.getItem('codexmateSessionTrashRetentionDays'));
477
483
  this.configTemplateDiffConfirmEnabled = loadConfigTemplateDiffConfirmEnabledFromStorage(localStorage);
478
484
  window.addEventListener('resize', this.onWindowResize);
479
485
  window.addEventListener('keydown', this.handleGlobalKeydown);
@@ -493,6 +499,22 @@ document.addEventListener('DOMContentLoaded', () => {
493
499
  console.error('加载 Claude 配置失败:', e);
494
500
  }
495
501
  }
502
+ {
503
+ const savedCurrentClaudeConfig = localStorage.getItem('currentClaudeConfig');
504
+ if (savedCurrentClaudeConfig && this.claudeConfigs[savedCurrentClaudeConfig]) {
505
+ this.currentClaudeConfig = savedCurrentClaudeConfig;
506
+ }
507
+ }
508
+ if (!this.currentClaudeConfig) {
509
+ const claudeConfigNames = Object.keys(this.claudeConfigs || {});
510
+ if (claudeConfigNames.length > 0) {
511
+ this.currentClaudeConfig = claudeConfigNames[0];
512
+ }
513
+ }
514
+ if (this.currentClaudeConfig && !this.currentClaudeModel) {
515
+ const initialClaudeConfig = this.claudeConfigs[this.currentClaudeConfig];
516
+ this.currentClaudeModel = initialClaudeConfig && initialClaudeConfig.model ? initialClaudeConfig.model : '';
517
+ }
496
518
  const normalizeOpenclawConfigs = (configs) => {
497
519
  const source = configs && typeof configs === 'object' && !Array.isArray(configs)
498
520
  ? configs
@@ -0,0 +1,70 @@
1
+ function stripLeadingSystemMessage(messages) {
2
+ if (!Array.isArray(messages) || messages.length < 2) return messages;
3
+ const first = messages[0];
4
+ if (!first || first.role !== 'system') return messages;
5
+ return messages.slice(1);
6
+ }
7
+
8
+ function toIsoTimestamp(value, fallback) {
9
+ const text = typeof value === 'string' ? value.trim() : '';
10
+ if (text) return text;
11
+ return typeof fallback === 'string' ? fallback : '';
12
+ }
13
+
14
+ export function normalizeSessionConvertSource(value) {
15
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
16
+ if (normalized === 'codex' || normalized === 'claude') return normalized;
17
+ return '';
18
+ }
19
+
20
+ export function getConvertTargetSource(source) {
21
+ return source === 'codex' ? 'claude' : (source === 'claude' ? 'codex' : '');
22
+ }
23
+
24
+ export function buildConvertedSessionJsonl(target, payload) {
25
+ const now = Date.now();
26
+ const baseTime = new Date(now).toISOString();
27
+ const sessionId = typeof payload.sessionId === 'string' ? payload.sessionId : '';
28
+ const cwd = typeof payload.cwd === 'string' ? payload.cwd : '';
29
+ const rawMessages = Array.isArray(payload.messages) ? payload.messages : [];
30
+ const messages = stripLeadingSystemMessage(rawMessages);
31
+ const lines = [];
32
+
33
+ if (target === 'codex') {
34
+ lines.push(JSON.stringify({ type: 'session_meta', timestamp: baseTime, payload: { id: sessionId, cwd } }));
35
+ for (let i = 0; i < messages.length; i += 1) {
36
+ const message = messages[i];
37
+ if (!message) continue;
38
+ const role = message.role === 'user' || message.role === 'assistant' || message.role === 'system'
39
+ ? message.role
40
+ : 'assistant';
41
+ const text = typeof message.text === 'string' ? message.text : '';
42
+ if (!text) continue;
43
+ lines.push(JSON.stringify({
44
+ type: 'response_item',
45
+ timestamp: toIsoTimestamp(message.timestamp, new Date(now + i).toISOString()),
46
+ payload: { type: 'message', role, content: text }
47
+ }));
48
+ }
49
+ return `${lines.join('\n')}\n`;
50
+ }
51
+
52
+ for (let i = 0; i < messages.length; i += 1) {
53
+ const message = messages[i];
54
+ if (!message) continue;
55
+ const role = message.role === 'user' || message.role === 'assistant' || message.role === 'system'
56
+ ? message.role
57
+ : 'assistant';
58
+ const text = typeof message.text === 'string' ? message.text : '';
59
+ if (!text) continue;
60
+ lines.push(JSON.stringify({
61
+ type: role,
62
+ timestamp: toIsoTimestamp(message.timestamp, new Date(now + i).toISOString()),
63
+ sessionId,
64
+ cwd,
65
+ message: { content: text }
66
+ }));
67
+ }
68
+ return `${lines.join('\n')}\n`;
69
+ }
70
+
@@ -206,6 +206,157 @@ function formatUtcDayKey(value) {
206
206
  return `${stamp.getUTCFullYear()}-${String(stamp.getUTCMonth() + 1).padStart(2, '0')}-${String(stamp.getUTCDate()).padStart(2, '0')}`;
207
207
  }
208
208
 
209
+ export function buildUsageHeatmap(sessions = [], options = {}) {
210
+ const list = Array.isArray(sessions) ? sessions : [];
211
+ const normalized = [];
212
+ for (const session of list) {
213
+ if (!session || typeof session !== 'object') continue;
214
+ const source = normalizeSessionSource(session.source, '');
215
+ if (source !== 'codex' && source !== 'claude') continue;
216
+ const updatedAtMs = Date.parse(session.updatedAt || '');
217
+ if (!Number.isFinite(updatedAtMs)) continue;
218
+ normalized.push({
219
+ updatedAtMs,
220
+ messageCount: Number.isFinite(Number(session.messageCount))
221
+ ? Math.max(0, Math.floor(Number(session.messageCount)))
222
+ : 0,
223
+ tokenTotal: readSessionTotalTokens(session)
224
+ });
225
+ }
226
+
227
+ const range = normalizeUsageRange(options.range);
228
+ const now = Number.isFinite(Number(options.now)) ? Number(options.now) : Date.now();
229
+ const dayMs = 24 * 60 * 60 * 1000;
230
+ const todayStart = toUtcDayStartMs(now);
231
+ let startDay = todayStart;
232
+ let endDay = todayStart;
233
+ if (range === 'all') {
234
+ const dayStarts = normalized.map((item) => toUtcDayStartMs(item.updatedAtMs)).filter((value) => Number.isFinite(value));
235
+ if (dayStarts.length) {
236
+ startDay = Math.min(...dayStarts);
237
+ endDay = Math.max(...dayStarts);
238
+ }
239
+ } else {
240
+ const rangeDays = range === '30d' ? 30 : 7;
241
+ endDay = todayStart;
242
+ startDay = todayStart - ((rangeDays - 1) * dayMs);
243
+ }
244
+
245
+ const startDow = new Date(startDay).getUTCDay();
246
+ const startShift = (startDow + 6) % 7;
247
+ const alignedStart = startDay - (startShift * dayMs);
248
+ const endDow = new Date(endDay).getUTCDay();
249
+ const endShift = (6 - ((endDow + 6) % 7));
250
+ const alignedEnd = endDay + (endShift * dayMs);
251
+ const totalDays = Math.floor((alignedEnd - alignedStart) / dayMs) + 1;
252
+ const weekCount = Math.max(1, Math.ceil(totalDays / 7));
253
+
254
+ const byDay = new Map();
255
+ for (const item of normalized) {
256
+ const dayKey = formatUtcDayKey(item.updatedAtMs);
257
+ const existing = byDay.get(dayKey) || { sessionCount: 0, messageCount: 0, tokenTotal: 0 };
258
+ existing.sessionCount += 1;
259
+ existing.messageCount += item.messageCount;
260
+ existing.tokenTotal += item.tokenTotal;
261
+ byDay.set(dayKey, existing);
262
+ }
263
+
264
+ const weeks = Array.from({ length: weekCount }, (_, idx) => ({
265
+ key: `w-${idx}`,
266
+ weekIndex: idx,
267
+ days: Array.from({ length: 7 }, () => null)
268
+ }));
269
+
270
+ let maxSessionCount = 0;
271
+ for (let dayIndex = 0; dayIndex < totalDays; dayIndex += 1) {
272
+ const dateMs = alignedStart + (dayIndex * dayMs);
273
+ const dateKey = formatUtcDayKey(dateMs);
274
+ const isInRange = dateMs >= startDay && dateMs <= endDay;
275
+ const weekIndex = Math.floor(dayIndex / 7);
276
+ const dow = new Date(dateMs).getUTCDay();
277
+ const rowIndex = (dow + 6) % 7;
278
+ const totals = isInRange ? (byDay.get(dateKey) || { sessionCount: 0, messageCount: 0, tokenTotal: 0 }) : null;
279
+ const sessionCount = totals ? totals.sessionCount : 0;
280
+ if (isInRange) {
281
+ maxSessionCount = Math.max(maxSessionCount, sessionCount);
282
+ }
283
+ weeks[weekIndex].days[rowIndex] = {
284
+ dateKey,
285
+ dateMs,
286
+ isInRange,
287
+ sessionCount,
288
+ messageCount: totals ? totals.messageCount : 0,
289
+ tokenTotal: totals ? totals.tokenTotal : 0
290
+ };
291
+ }
292
+
293
+ return {
294
+ range,
295
+ startDay,
296
+ endDay,
297
+ alignedStart,
298
+ alignedEnd,
299
+ maxSessionCount,
300
+ weeks
301
+ };
302
+ }
303
+
304
+ export function buildUsageHourlyHeatmap(sessions = [], options = {}) {
305
+ const list = Array.isArray(sessions) ? sessions : [];
306
+ const range = normalizeUsageRange(options.range);
307
+ const now = Number.isFinite(Number(options.now)) ? Number(options.now) : Date.now();
308
+ const dayMs = 24 * 60 * 60 * 1000;
309
+ const todayStart = toUtcDayStartMs(now);
310
+
311
+ const normalized = [];
312
+ for (const session of list) {
313
+ if (!session || typeof session !== 'object') continue;
314
+ const source = normalizeSessionSource(session.source, '');
315
+ if (source !== 'codex' && source !== 'claude') continue;
316
+ const updatedAtMs = Date.parse(session.updatedAt || '');
317
+ if (!Number.isFinite(updatedAtMs)) continue;
318
+ const dayStart = toUtcDayStartMs(updatedAtMs);
319
+ if (range !== 'all') {
320
+ const rangeDays = range === '30d' ? 30 : 7;
321
+ const rangeStart = todayStart - ((rangeDays - 1) * dayMs);
322
+ if (dayStart < rangeStart || dayStart > todayStart) continue;
323
+ }
324
+ const stamp = new Date(updatedAtMs);
325
+ const weekday = (stamp.getUTCDay() + 6) % 7;
326
+ const hour = stamp.getUTCHours();
327
+ const messageCount = Number.isFinite(Number(session.messageCount))
328
+ ? Math.max(0, Math.floor(Number(session.messageCount)))
329
+ : 0;
330
+ const tokenTotal = readSessionTotalTokens(session);
331
+ normalized.push({ weekday, hour, messageCount, tokenTotal });
332
+ }
333
+
334
+ const grid = Array.from({ length: 7 }, () =>
335
+ Array.from({ length: 24 }, () => ({ sessionCount: 0, messageCount: 0, tokenTotal: 0 }))
336
+ );
337
+ for (const item of normalized) {
338
+ const cell = grid[item.weekday][item.hour];
339
+ cell.sessionCount += 1;
340
+ cell.messageCount += item.messageCount;
341
+ cell.tokenTotal += item.tokenTotal;
342
+ }
343
+
344
+ let maxSessionCount = 0;
345
+ for (let day = 0; day < 7; day += 1) {
346
+ for (let hour = 0; hour < 24; hour += 1) {
347
+ maxSessionCount = Math.max(maxSessionCount, grid[day][hour].sessionCount);
348
+ }
349
+ }
350
+
351
+ return {
352
+ range,
353
+ grid,
354
+ maxSessionCount: Math.max(1, maxSessionCount),
355
+ weekdayKeys: [0, 1, 2, 3, 4, 5, 6],
356
+ hourLabels: Array.from({ length: 24 }, (_, index) => String(index).padStart(2, '0'))
357
+ };
358
+ }
359
+
209
360
  function buildUsageBuckets(normalizedSessions, options = {}) {
210
361
  const range = normalizeUsageRange(options.range);
211
362
  const now = Number.isFinite(Number(options.now)) ? Number(options.now) : Date.now();
@@ -40,7 +40,50 @@ export function createDashboardComputed() {
40
40
  return list;
41
41
  },
42
42
  installTargetCards() {
43
- const targets = Array.isArray(this.installStatusTargets) ? this.installStatusTargets : [];
43
+ const targets = Array.isArray(this.installStatusTargets) && this.installStatusTargets.length
44
+ ? this.installStatusTargets
45
+ : [
46
+ {
47
+ id: 'claude',
48
+ name: 'Claude Code CLI',
49
+ packageName: '@anthropic-ai/claude-code',
50
+ installed: false,
51
+ bin: 'claude',
52
+ version: '',
53
+ commandPath: '',
54
+ error: ''
55
+ },
56
+ {
57
+ id: 'codebuddy',
58
+ name: 'CodeBuddy Code',
59
+ packageName: '@tencent-ai/codebuddy-code',
60
+ installed: false,
61
+ bin: 'codebuddy',
62
+ version: '',
63
+ commandPath: '',
64
+ error: ''
65
+ },
66
+ {
67
+ id: 'gemini',
68
+ name: 'Gemini CLI',
69
+ packageName: '@google/gemini-cli',
70
+ installed: false,
71
+ bin: 'gemini',
72
+ version: '',
73
+ commandPath: '',
74
+ error: ''
75
+ },
76
+ {
77
+ id: 'codex',
78
+ name: 'Codex CLI',
79
+ packageName: '@openai/codex',
80
+ installed: false,
81
+ bin: 'codex',
82
+ version: '',
83
+ commandPath: '',
84
+ error: ''
85
+ }
86
+ ];
44
87
  const action = this.normalizeInstallAction(this.installCommandAction);
45
88
  return targets.map((target) => {
46
89
  const id = target && typeof target.id === 'string' ? target.id : '';