replit-tools 1.2.23 → 1.2.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replit-tools",
3
- "version": "1.2.23",
3
+ "version": "1.2.26",
4
4
  "description": "DATA Tools - One command to set up Claude Code and Codex CLI on Replit with full persistence",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "keywords": [
10
10
  "replit",
11
11
  "claude",
12
+ "codex",
12
13
  "claude-code",
13
14
  "persistence",
14
15
  "session",
@@ -26,44 +26,42 @@ get_terminal_id() {
26
26
  TERMINAL_ID=$(get_terminal_id)
27
27
  STATE_FILE="${SESSIONS_DIR}/${TERMINAL_ID}.json"
28
28
 
29
- # Get recent sessions with full details
29
+ # Get recent sessions with full details (both Claude and Codex combined, sorted by last seen)
30
30
  get_recent_sessions() {
31
31
  local history="${HOME}/.claude/history.jsonl"
32
32
  local projects_dir="${HOME}/.claude/projects/-home-runner-workspace"
33
+ local codex_sessions_dir="${HOME}/.codex/sessions"
33
34
 
34
- if [ -f "${history}" ]; then
35
- # Collect all session data with full metadata
36
- node -e "
37
- const fs = require('fs');
38
- const path = require('path');
39
- const readline = require('readline');
35
+ node -e "
36
+ const fs = require('fs');
37
+ const path = require('path');
40
38
 
41
- const historyFile = '${history}';
42
- const projectsDir = '${projects_dir}';
39
+ const historyFile = '${history}';
40
+ const projectsDir = '${projects_dir}';
41
+ const codexSessionsDir = '${codex_sessions_dir}';
43
42
 
44
- const sessionData = new Map();
43
+ const sessionData = new Map();
45
44
 
46
- // Read history to get session metadata
45
+ // --- Claude sessions ---
46
+ if (fs.existsSync(historyFile)) {
47
47
  const lines = fs.readFileSync(historyFile, 'utf8').trim().split('\n');
48
-
49
48
  for (const line of lines) {
50
49
  try {
51
50
  const j = JSON.parse(line);
52
51
  if (!j.sessionId) continue;
53
-
54
- if (!sessionData.has(j.sessionId)) {
55
- sessionData.set(j.sessionId, {
52
+ const key = 'claude:' + j.sessionId;
53
+ if (!sessionData.has(key)) {
54
+ sessionData.set(key, {
55
+ tool: 'claude',
56
56
  id: j.sessionId,
57
57
  firstSeen: j.timestamp,
58
58
  lastSeen: j.timestamp,
59
59
  firstPrompt: j.display || '',
60
60
  lastPrompt: j.display || '',
61
- messageCount: 0,
62
- project: j.project || ''
61
+ messageCount: 0
63
62
  });
64
63
  }
65
-
66
- const data = sessionData.get(j.sessionId);
64
+ const data = sessionData.get(key);
67
65
  if (j.timestamp < data.firstSeen) {
68
66
  data.firstSeen = j.timestamp;
69
67
  data.firstPrompt = j.display || data.firstPrompt;
@@ -75,76 +73,123 @@ get_recent_sessions() {
75
73
  } catch(e) {}
76
74
  }
77
75
 
78
- // Enrich with .jsonl file data (message counts, file sizes)
79
- for (const [id, data] of sessionData) {
80
- const jsonlPath = path.join(projectsDir, id + '.jsonl');
81
- const agentPath = path.join(projectsDir, 'agent-' + id.substring(0,7) + '.jsonl');
82
-
83
- let filePath = null;
84
- let fileSize = 0;
85
-
86
- if (fs.existsSync(jsonlPath)) {
87
- filePath = jsonlPath;
88
- } else if (fs.existsSync(agentPath)) {
89
- filePath = agentPath;
90
- }
91
-
76
+ for (const [key, data] of sessionData) {
77
+ if (data.tool !== 'claude') continue;
78
+ const jsonlPath = path.join(projectsDir, data.id + '.jsonl');
79
+ const agentPath = path.join(projectsDir, 'agent-' + data.id.substring(0,7) + '.jsonl');
80
+ let filePath = fs.existsSync(jsonlPath) ? jsonlPath : (fs.existsSync(agentPath) ? agentPath : null);
92
81
  if (filePath) {
93
82
  try {
94
83
  const stat = fs.statSync(filePath);
95
- fileSize = stat.size;
84
+ data.fileSize = stat.size;
96
85
  const content = fs.readFileSync(filePath, 'utf8');
97
- const msgLines = content.trim().split('\n').filter(l => l.trim());
98
- data.messageCount = msgLines.length;
99
- data.fileSize = fileSize;
100
- data.filePath = filePath;
86
+ data.messageCount = content.trim().split('\n').filter(l => l.trim()).length;
101
87
  } catch(e) {}
102
88
  }
103
89
  }
90
+ }
104
91
 
105
- // Sort by lastSeen descending and output
106
- const sorted = Array.from(sessionData.values())
107
- .sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0))
108
- .slice(0, 10);
109
-
110
- sorted.forEach((s, i) => {
111
- const formatTime = (ts) => {
112
- if (!ts) return 'unknown';
113
- const d = new Date(ts);
114
- const utc = d.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
115
- // MST is UTC-7
116
- const mst = new Date(ts - 7*60*60*1000).toISOString().replace('T', ' ').substring(0, 19) + ' MST';
117
- return utc + ' / ' + mst;
118
- };
119
-
120
- const timeAgo = (ts) => {
121
- if (!ts) return '';
122
- const mins = Math.round((Date.now() - ts) / 1000 / 60);
123
- if (mins < 60) return mins + 'm ago';
124
- if (mins < 1440) return Math.round(mins/60) + 'h ago';
125
- return Math.round(mins/1440) + 'd ago';
126
- };
127
-
128
- const sizeStr = (bytes) => {
129
- if (!bytes) return '0B';
130
- if (bytes < 1024) return bytes + 'B';
131
- if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + 'KB';
132
- return (bytes/1024/1024).toFixed(1) + 'MB';
133
- };
134
-
135
- console.log('SESSION|' + (i+1));
136
- console.log('ID|' + s.id);
137
- console.log('MESSAGES|' + (s.messageCount || '?'));
138
- console.log('SIZE|' + sizeStr(s.fileSize));
139
- console.log('LAST_ACTIVE|' + timeAgo(s.lastSeen));
140
- console.log('STARTED|' + formatTime(s.firstSeen));
141
- console.log('LAST_SEEN|' + formatTime(s.lastSeen));
142
- console.log('FIRST_PROMPT|' + (s.firstPrompt || '').substring(0, 80).replace(/\\n/g, ' ').trim());
143
- console.log('LAST_PROMPT|' + (s.lastPrompt || '').substring(0, 80).replace(/\\n/g, ' ').trim());
144
- console.log('---');
145
- });
146
- " 2>/dev/null
147
- fi
92
+ // --- Codex sessions (walk YYYY/MM/DD tree, filter to cwd) ---
93
+ const cwd = '/home/runner/workspace';
94
+ if (fs.existsSync(codexSessionsDir)) {
95
+ const walk = (dir) => {
96
+ let results = [];
97
+ try {
98
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
99
+ const full = path.join(dir, entry.name);
100
+ if (entry.isDirectory()) results = results.concat(walk(full));
101
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) results.push(full);
102
+ }
103
+ } catch(e) {}
104
+ return results;
105
+ };
106
+ const files = walk(codexSessionsDir);
107
+ for (const f of files) {
108
+ try {
109
+ const content = fs.readFileSync(f, 'utf8');
110
+ const lines = content.trim().split('\n');
111
+ if (!lines.length) continue;
112
+
113
+ const meta = JSON.parse(lines[0]);
114
+ if (meta.type !== 'session_meta' || !meta.payload) continue;
115
+ if (meta.payload.cwd !== cwd) continue;
116
+
117
+ const id = meta.payload.id;
118
+ const firstTs = Date.parse(meta.payload.timestamp || meta.timestamp);
119
+
120
+ let lastTs = firstTs;
121
+ let firstPrompt = '';
122
+ let lastPrompt = '';
123
+ let msgCount = 0;
124
+ for (const ln of lines) {
125
+ try {
126
+ const j = JSON.parse(ln);
127
+ if (j.timestamp) lastTs = Math.max(lastTs, Date.parse(j.timestamp));
128
+ if (j.type === 'response_item' && j.payload && j.payload.role === 'user' && Array.isArray(j.payload.content)) {
129
+ const text = (j.payload.content.find(c => c.type === 'input_text') || {}).text || '';
130
+ if (text && !text.startsWith('<user_instructions>') && !text.startsWith('<environment_context>')) {
131
+ if (!firstPrompt) firstPrompt = text;
132
+ lastPrompt = text;
133
+ msgCount++;
134
+ }
135
+ }
136
+ } catch(e) {}
137
+ }
138
+
139
+ const stat = fs.statSync(f);
140
+ sessionData.set('codex:' + id, {
141
+ tool: 'codex',
142
+ id,
143
+ firstSeen: firstTs,
144
+ lastSeen: lastTs,
145
+ firstPrompt,
146
+ lastPrompt,
147
+ messageCount: msgCount,
148
+ fileSize: stat.size
149
+ });
150
+ } catch(e) {}
151
+ }
152
+ }
153
+
154
+ // Sort by lastSeen descending
155
+ const sorted = Array.from(sessionData.values())
156
+ .sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0))
157
+ .slice(0, 10);
158
+
159
+ const formatTime = (ts) => {
160
+ if (!ts) return 'unknown';
161
+ const utc = new Date(ts).toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
162
+ const mst = new Date(ts - 7*60*60*1000).toISOString().replace('T', ' ').substring(0, 19) + ' MST';
163
+ return utc + ' / ' + mst;
164
+ };
165
+ const timeAgo = (ts) => {
166
+ if (!ts) return '';
167
+ const mins = Math.round((Date.now() - ts) / 1000 / 60);
168
+ if (mins < 60) return mins + 'm ago';
169
+ if (mins < 1440) return Math.round(mins/60) + 'h ago';
170
+ return Math.round(mins/1440) + 'd ago';
171
+ };
172
+ const sizeStr = (bytes) => {
173
+ if (!bytes) return '0B';
174
+ if (bytes < 1024) return bytes + 'B';
175
+ if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + 'KB';
176
+ return (bytes/1024/1024).toFixed(1) + 'MB';
177
+ };
178
+
179
+ sorted.forEach((s, i) => {
180
+ console.log('SESSION|' + (i+1));
181
+ console.log('TOOL|' + s.tool);
182
+ console.log('ID|' + s.id);
183
+ console.log('MESSAGES|' + (s.messageCount || '?'));
184
+ console.log('SIZE|' + sizeStr(s.fileSize));
185
+ console.log('LAST_ACTIVE|' + timeAgo(s.lastSeen));
186
+ console.log('STARTED|' + formatTime(s.firstSeen));
187
+ console.log('LAST_SEEN|' + formatTime(s.lastSeen));
188
+ console.log('FIRST_PROMPT|' + (s.firstPrompt || '').substring(0, 80).replace(/\\n/g, ' ').trim());
189
+ console.log('LAST_PROMPT|' + (s.lastPrompt || '').substring(0, 80).replace(/\\n/g, ' ').trim());
190
+ console.log('---');
191
+ });
192
+ " 2>/dev/null
148
193
  }
149
194
 
150
195
  # Display formatted session list
@@ -165,6 +210,13 @@ show_sessions() {
165
210
  echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━"
166
211
  echo " [$value]"
167
212
  ;;
213
+ TOOL)
214
+ if [ "$value" = "codex" ]; then
215
+ echo -e " Tool: \033[1;38;5;208mcodex\033[0m"
216
+ else
217
+ echo -e " Tool: \033[1;38;5;33mclaude\033[0m"
218
+ fi
219
+ ;;
168
220
  ID)
169
221
  echo " ID: $value"
170
222
  ;;
@@ -198,48 +250,187 @@ show_sessions() {
198
250
  echo ""
199
251
  }
200
252
 
201
- # Count running Claude instances
253
+ # Count running Claude/Codex instances
202
254
  count_claude_instances() {
203
255
  pgrep -x "claude" 2>/dev/null | wc -l
204
256
  }
257
+ count_codex_instances() {
258
+ pgrep -x "codex" 2>/dev/null | wc -l
259
+ }
205
260
 
206
- # Save session state
261
+ # Save session state (tool = "claude" or "codex")
207
262
  save_session_state() {
208
263
  local session_id="$1"
209
- local flags="${2:---dangerously-skip-permissions}"
264
+ local tool="${2:-claude}"
210
265
  mkdir -p "${SESSIONS_DIR}"
211
266
  cat > "${STATE_FILE}" << EOF
212
267
  {
213
268
  "sessionId": "${session_id}",
214
- "flags": "${flags}",
269
+ "tool": "${tool}",
215
270
  "terminalId": "${TERMINAL_ID}",
216
271
  "timestamp": $(date +%s)
217
272
  }
218
273
  EOF
219
274
  }
220
275
 
221
- # Get last session for this terminal
276
+ # Get last session for this terminal (prints "<tool>|<sessionId>")
222
277
  get_terminal_last_session() {
223
278
  if [ -f "${STATE_FILE}" ]; then
224
- node -e "try{console.log(require('${STATE_FILE}').sessionId||'')}catch(e){}" 2>/dev/null
279
+ node -e "try{const s=require('${STATE_FILE}');console.log((s.tool||'claude')+'|'+(s.sessionId||''))}catch(e){}" 2>/dev/null
225
280
  fi
226
281
  }
227
282
 
228
- # Run Claude and handle exit codes (returns to menu on failure)
229
- run_claude_with_retry() {
283
+ # Recent sessions within last 24h (max 9). Prints "NUM|TOOL|ID|SNIPPET" per line.
284
+ get_recent_24h_sessions() {
285
+ local history="${HOME}/.claude/history.jsonl"
286
+ local projects_dir="${HOME}/.claude/projects/-home-runner-workspace"
287
+ local codex_sessions_dir="${HOME}/.codex/sessions"
288
+
289
+ node -e "
290
+ const fs = require('fs');
291
+ const path = require('path');
292
+ const cutoff = Date.now() - 24*60*60*1000;
293
+ const cwd = '/home/runner/workspace';
294
+ const sessions = new Map();
295
+
296
+ // Claude
297
+ const historyFile = '${history}';
298
+ if (fs.existsSync(historyFile)) {
299
+ const lines = fs.readFileSync(historyFile, 'utf8').trim().split('\n');
300
+ for (const line of lines) {
301
+ try {
302
+ const j = JSON.parse(line);
303
+ if (!j.sessionId || !j.timestamp) continue;
304
+ const key = 'claude:' + j.sessionId;
305
+ if (!sessions.has(key)) {
306
+ sessions.set(key, { tool: 'claude', id: j.sessionId, firstSeen: j.timestamp, lastSeen: j.timestamp, firstPrompt: j.display || '' });
307
+ }
308
+ const s = sessions.get(key);
309
+ if (j.timestamp < s.firstSeen) { s.firstSeen = j.timestamp; s.firstPrompt = j.display || s.firstPrompt; }
310
+ if (j.timestamp > s.lastSeen) { s.lastSeen = j.timestamp; }
311
+ } catch(e) {}
312
+ }
313
+ }
314
+
315
+ // Codex
316
+ const codexDir = '${codex_sessions_dir}';
317
+ if (fs.existsSync(codexDir)) {
318
+ const walk = (d) => {
319
+ let r = [];
320
+ try {
321
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
322
+ const f = path.join(d, e.name);
323
+ if (e.isDirectory()) r = r.concat(walk(f));
324
+ else if (e.isFile() && e.name.endsWith('.jsonl')) r.push(f);
325
+ }
326
+ } catch(e) {}
327
+ return r;
328
+ };
329
+ for (const f of walk(codexDir)) {
330
+ try {
331
+ const stat = fs.statSync(f);
332
+ if (stat.mtimeMs < cutoff) continue;
333
+ const content = fs.readFileSync(f, 'utf8');
334
+ const lns = content.trim().split('\n');
335
+ if (!lns.length) continue;
336
+ const meta = JSON.parse(lns[0]);
337
+ if (meta.type !== 'session_meta' || !meta.payload) continue;
338
+ if (meta.payload.cwd !== cwd) continue;
339
+ const id = meta.payload.id;
340
+ const firstTs = Date.parse(meta.payload.timestamp || meta.timestamp);
341
+ let lastTs = firstTs;
342
+ let firstPrompt = '';
343
+ for (const ln of lns) {
344
+ try {
345
+ const j = JSON.parse(ln);
346
+ if (j.timestamp) lastTs = Math.max(lastTs, Date.parse(j.timestamp));
347
+ if (!firstPrompt && j.type === 'response_item' && j.payload && j.payload.role === 'user' && Array.isArray(j.payload.content)) {
348
+ const text = (j.payload.content.find(c => c.type === 'input_text') || {}).text || '';
349
+ if (text && !text.startsWith('<user_instructions>') && !text.startsWith('<environment_context>')) {
350
+ firstPrompt = text;
351
+ }
352
+ }
353
+ } catch(e) {}
354
+ }
355
+ sessions.set('codex:' + id, { tool: 'codex', id, firstSeen: firstTs, lastSeen: lastTs, firstPrompt });
356
+ } catch(e) {}
357
+ }
358
+ }
359
+
360
+ const normTs = (t) => typeof t === 'number' ? t : (Date.parse(t) || 0);
361
+ const words = (s) => (s || '').replace(/\s+/g, ' ').trim().split(' ').slice(0, 5).join(' ');
362
+ const ago = (ts) => {
363
+ const mins = Math.round((Date.now() - ts) / 60000);
364
+ if (mins < 1) return 'just now';
365
+ if (mins < 60) return mins + 'm ago';
366
+ const h = Math.round(mins / 60);
367
+ return h + 'h ago';
368
+ };
369
+ const sorted = Array.from(sessions.values())
370
+ .map(s => ({ ...s, lastSeen: normTs(s.lastSeen) }))
371
+ .filter(s => s.lastSeen >= cutoff)
372
+ .sort((a, b) => b.lastSeen - a.lastSeen)
373
+ .slice(0, 9);
374
+
375
+ sorted.forEach((s, i) => {
376
+ console.log((i+1) + '|' + s.tool + '|' + s.id + '|' + ago(s.lastSeen) + '|' + words(s.firstPrompt));
377
+ });
378
+ " 2>/dev/null
379
+ }
380
+
381
+ # Latest Codex session ID for this cwd (fallback when no terminal state exists)
382
+ get_latest_codex_session() {
383
+ node -e "
384
+ const fs = require('fs');
385
+ const path = require('path');
386
+ const dir = '${HOME}/.codex/sessions';
387
+ const cwd = '/home/runner/workspace';
388
+ if (!fs.existsSync(dir)) process.exit(0);
389
+ const walk = (d) => {
390
+ let r = [];
391
+ try {
392
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
393
+ const f = path.join(d, e.name);
394
+ if (e.isDirectory()) r = r.concat(walk(f));
395
+ else if (e.isFile() && e.name.endsWith('.jsonl')) r.push(f);
396
+ }
397
+ } catch(e) {}
398
+ return r;
399
+ };
400
+ let best = null;
401
+ for (const f of walk(dir)) {
402
+ try {
403
+ const first = fs.readFileSync(f, 'utf8').split('\n', 1)[0];
404
+ const meta = JSON.parse(first);
405
+ if (meta.type !== 'session_meta' || !meta.payload) continue;
406
+ if (meta.payload.cwd !== cwd) continue;
407
+ const ts = fs.statSync(f).mtimeMs;
408
+ if (!best || ts > best.ts) best = { id: meta.payload.id, ts };
409
+ } catch(e) {}
410
+ }
411
+ if (best) console.log(best.id);
412
+ " 2>/dev/null
413
+ }
414
+
415
+ # Run a CLI command and update session state on clean exit
416
+ run_tool_with_retry() {
230
417
  local cmd="$1"
231
418
  local session_desc="$2"
419
+ local tool="${3:-claude}"
232
420
 
233
421
  echo ""
234
422
  echo " ${session_desc}..."
235
423
 
236
- # Run Claude
237
424
  eval "$cmd"
238
425
  local exit_code=$?
239
426
 
240
- # Save session state on success
241
427
  if [ $exit_code -eq 0 ]; then
242
- save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)"
428
+ if [ "$tool" = "codex" ]; then
429
+ local cid=$(get_latest_codex_session)
430
+ [ -n "$cid" ] && save_session_state "$cid" "codex"
431
+ else
432
+ save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)" "claude"
433
+ fi
243
434
  fi
244
435
 
245
436
  return $exit_code
@@ -250,8 +441,12 @@ claude_prompt() {
250
441
  # Only in interactive shells
251
442
  [[ $- != *i* ]] && return 0
252
443
 
253
- # Check if claude exists
254
- if ! command -v claude &>/dev/null; then
444
+ # Need at least one of the tools
445
+ local has_claude=0
446
+ local has_codex=0
447
+ command -v claude &>/dev/null && has_claude=1
448
+ command -v codex &>/dev/null && has_codex=1
449
+ if [ "$has_claude" -eq 0 ] && [ "$has_codex" -eq 0 ]; then
255
450
  return 0
256
451
  fi
257
452
 
@@ -284,8 +479,12 @@ claude_prompt() {
284
479
 
285
480
  # Main menu loop - keeps showing until user chooses shell
286
481
  while true; do
287
- local running=$(count_claude_instances)
288
- local last_session=$(get_terminal_last_session)
482
+ local claude_running=$(count_claude_instances)
483
+ local codex_running=$(count_codex_instances)
484
+ local last_entry=$(get_terminal_last_session)
485
+ local last_tool="${last_entry%%|*}"
486
+ local last_session="${last_entry#*|}"
487
+ [ "$last_tool" = "$last_entry" ] && last_tool=""
289
488
 
290
489
  # Colored command key (shown first)
291
490
  echo ""
@@ -293,60 +492,111 @@ claude_prompt() {
293
492
  echo -e " │ \033[95mAt \033[1;38;5;33m~/workspace\033[0;97m\$\033[95m prompt:\033[0m │"
294
493
  echo -e " │ \033[96mclaude-menu\033[0m = show menu │"
295
494
  echo -e " │ \033[96mcm\033[0m = menu shortcut │"
296
- echo -e " │ \033[96ml\033[0m = login to claude │"
495
+ echo -e " │ \033[96mj\033[0m = login to claude │"
496
+ echo -e " │ \033[96mk\033[0m = login to codex │"
297
497
  echo " ├─────────────────────────────┤"
298
498
  echo -e " │ \033[1;38;5;208mIn Claude:\033[0m │"
299
499
  echo -e " │ \033[92mCtrl+C x2\033[0m = back to menu │"
300
500
  echo -e " │ \033[92mCtrl+C x3\033[0m = exit to shell │"
301
501
  echo " └─────────────────────────────┘"
302
502
 
503
+ # Recent Sessions (last 24h) - numbered for instant resume
504
+ local recent_24h=$(get_recent_24h_sessions)
505
+ local recent_tools=()
506
+ local recent_ids=()
507
+ if [ -n "$recent_24h" ]; then
508
+ echo ""
509
+ echo -e " \033[1mRecent (last 24h):\033[0m"
510
+ while IFS='|' read -r num tool id when snippet; do
511
+ [ -z "$num" ] && continue
512
+ recent_tools[$num]="$tool"
513
+ recent_ids[$num]="$id"
514
+ local label_color="\033[1;38;5;33m"
515
+ local label="cld"
516
+ if [ "$tool" = "codex" ]; then
517
+ label_color="\033[1;38;5;208m"
518
+ label="cdx"
519
+ fi
520
+ printf " \033[1;97m[%s]\033[0m ${label_color}%s\033[0m \033[2m%-8s\033[0m %s\n" "$num" "$label" "$when" "$snippet"
521
+ done <<< "$recent_24h"
522
+ fi
523
+
303
524
  echo ""
304
525
  echo " ┌─────────────────────────────┐"
305
- echo " │ Claude Session Manager │"
526
+ echo " │ DATA Session Manager │"
306
527
  echo " └─────────────────────────────┘"
307
528
 
308
- if [ "$running" -gt 0 ]; then
309
- echo " ($running running)"
529
+ local running_note=""
530
+ [ "$claude_running" -gt 0 ] && running_note="${claude_running} claude"
531
+ if [ "$codex_running" -gt 0 ]; then
532
+ [ -n "$running_note" ] && running_note="${running_note}, "
533
+ running_note="${running_note}${codex_running} codex"
310
534
  fi
535
+ [ -n "$running_note" ] && echo " (${running_note} running)"
311
536
  echo ""
312
537
 
313
538
  # Show options
314
539
  echo " [c] Continue last session"
315
540
  if [ -n "$last_session" ]; then
316
- echo " └─ ${last_session:0:8}..."
541
+ echo " └─ ${last_tool}:${last_session:0:8}..."
317
542
  fi
318
- echo " [r] Resume (pick from list)"
319
- echo " [n] Start new session"
320
- echo " [l] Login to Claude"
543
+ [ -n "$recent_24h" ] && echo " [1-9] Resume numbered above"
544
+ echo " [r] Resume (full list)"
545
+ echo " [n] New Claude session"
546
+ echo " [m] New Codex session"
547
+ echo " [j] Login to Claude"
548
+ echo " [k] Login to Codex"
321
549
  echo " [s] Skip - just shell"
322
550
  echo ""
323
551
 
324
552
  # Read choice with timeout
325
553
  local choice
326
- read -t 60 -n 1 -p " Choice [c/r/n/l/s]: " choice
554
+ read -t 60 -n 1 -p " Choice: " choice
327
555
  echo ""
328
556
 
329
557
  case "$choice" in
558
+ [1-9])
559
+ local sel_tool="${recent_tools[$choice]}"
560
+ local sel_id="${recent_ids[$choice]}"
561
+ if [ -n "$sel_id" ]; then
562
+ if [ "$sel_tool" = "codex" ]; then
563
+ run_tool_with_retry "codex resume '$sel_id'" "Resuming codex session ${sel_id:0:8}" "codex"
564
+ else
565
+ run_tool_with_retry "claude -r '$sel_id' --dangerously-skip-permissions" "Resuming claude session ${sel_id:0:8}" "claude"
566
+ fi
567
+ echo ""
568
+ echo " Exited. Returning to menu..."
569
+ sleep 1
570
+ else
571
+ echo " No session at position $choice."
572
+ sleep 1
573
+ fi
574
+ ;;
330
575
  c|C|"")
331
- # Continue last session (default on Enter or timeout)
576
+ # Continue whichever tool was most recent in this shell
332
577
  if [ -n "$last_session" ]; then
333
- run_claude_with_retry "claude -r '$last_session' --dangerously-skip-permissions" "Resuming session ${last_session:0:8}"
578
+ if [ "$last_tool" = "codex" ]; then
579
+ run_tool_with_retry "codex resume '$last_session'" "Resuming codex session ${last_session:0:8}" "codex"
580
+ else
581
+ run_tool_with_retry "claude -r '$last_session' --dangerously-skip-permissions" "Resuming claude session ${last_session:0:8}" "claude"
582
+ fi
334
583
  else
335
- run_claude_with_retry "claude --dangerously-skip-permissions" "No previous session, starting new"
584
+ run_tool_with_retry "claude --dangerously-skip-permissions" "No previous session, starting new Claude" "claude"
336
585
  fi
337
- # After Claude exits, loop back to menu
338
586
  echo ""
339
- echo " Claude exited. Returning to menu..."
587
+ echo " Exited. Returning to menu..."
340
588
  sleep 1
341
589
  ;;
342
590
  r|R)
343
- # Show session list with full details
591
+ # Combined session list (claude + codex)
344
592
  echo ""
345
593
  echo " Recent Sessions"
346
594
  show_sessions
347
595
 
348
- # Get session IDs for selection
349
- local session_ids=$(get_recent_sessions | grep "^ID|" | cut -d'|' -f2)
596
+ # Parallel arrays of tools and ids (index-aligned to SESSION numbers)
597
+ local data=$(get_recent_sessions)
598
+ local tools_list=$(echo "$data" | grep "^TOOL|" | cut -d'|' -f2)
599
+ local ids_list=$(echo "$data" | grep "^ID|" | cut -d'|' -f2)
350
600
 
351
601
  read -t 60 -p " Enter number (or 'q' to cancel): " session_num
352
602
 
@@ -355,49 +605,60 @@ claude_prompt() {
355
605
  continue
356
606
  fi
357
607
 
358
- local selected_id=$(echo "$session_ids" | sed -n "${session_num}p")
608
+ local selected_tool=$(echo "$tools_list" | sed -n "${session_num}p")
609
+ local selected_id=$(echo "$ids_list" | sed -n "${session_num}p")
359
610
  if [ -n "$selected_id" ]; then
360
- run_claude_with_retry "claude -r '$selected_id' --dangerously-skip-permissions" "Resuming session $selected_id"
611
+ if [ "$selected_tool" = "codex" ]; then
612
+ run_tool_with_retry "codex resume '$selected_id'" "Resuming codex session $selected_id" "codex"
613
+ else
614
+ run_tool_with_retry "claude -r '$selected_id' --dangerously-skip-permissions" "Resuming claude session $selected_id" "claude"
615
+ fi
361
616
  echo ""
362
- echo " Claude exited. Returning to menu..."
617
+ echo " Exited. Returning to menu..."
363
618
  sleep 1
364
619
  else
365
620
  echo " Invalid selection."
366
621
  fi
367
622
  ;;
368
623
  n|N)
369
- # Start new session
370
- run_claude_with_retry "claude --dangerously-skip-permissions" "Starting new Claude session"
624
+ run_tool_with_retry "claude --dangerously-skip-permissions" "Starting new Claude session" "claude"
371
625
  echo ""
372
- echo " Claude exited. Returning to menu..."
626
+ echo " Exited. Returning to menu..."
373
627
  sleep 1
374
628
  ;;
375
- l|L)
376
- # Login to Claude
629
+ m|M)
630
+ run_tool_with_retry "codex" "Starting new Codex session" "codex"
631
+ echo ""
632
+ echo " Exited. Returning to menu..."
633
+ sleep 1
634
+ ;;
635
+ j|J|l|L)
377
636
  echo ""
378
637
  echo " Starting Claude login..."
379
638
  echo ""
380
-
381
- # Clear the auth failed marker so setup script will retry
382
639
  rm -f "${REPLIT_TOOLS}/.auth-refresh-failed" 2>/dev/null
383
-
384
640
  claude /login --dangerously-skip-permissions
385
-
641
+ echo ""
642
+ echo " Login complete. Returning to menu..."
643
+ sleep 1
644
+ ;;
645
+ k|K)
646
+ echo ""
647
+ echo " Starting Codex login..."
648
+ echo ""
649
+ codex login
386
650
  echo ""
387
651
  echo " Login complete. Returning to menu..."
388
652
  sleep 1
389
653
  ;;
390
654
  s|S)
391
- # Skip - just shell
392
655
  echo ""
393
656
  echo " Okay, just a shell. Type 'claude-menu' to return here."
394
- # Keep the marker so re-sourcing bashrc won't re-show menu
395
- # The trap will clean it up when shell exits
396
657
  return 0
397
658
  ;;
398
659
  *)
399
660
  echo ""
400
- echo " Unknown option. Please choose c, r, n, l, or s."
661
+ echo " Unknown option. Choose 1-9, c, r, n, m, j, k, or s."
401
662
  sleep 1
402
663
  ;;
403
664
  esac
@@ -409,8 +670,12 @@ alias cr='claude -c --dangerously-skip-permissions'
409
670
  alias claude-resume='claude -c --dangerously-skip-permissions'
410
671
  alias claude-pick='claude -r --dangerously-skip-permissions'
411
672
  alias claude-new='claude --dangerously-skip-permissions'
412
- alias l='claude /login --dangerously-skip-permissions'
673
+ alias j='claude /login --dangerously-skip-permissions'
413
674
  alias claude-login='claude /login --dangerously-skip-permissions'
675
+ alias k='codex login'
676
+ alias codex-login='codex login'
677
+ alias codex-new='codex'
678
+ alias codex-resume='codex resume'
414
679
 
415
680
  # Export for manual use
416
681
  export -f get_recent_sessions