replit-tools 1.2.22 → 1.2.25

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.22",
3
+ "version": "1.2.25",
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,178 @@ 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 words = (s) => (s || '').replace(/\s+/g, ' ').trim().split(' ').slice(0, 5).join(' ');
361
+ const sorted = Array.from(sessions.values())
362
+ .filter(s => s.lastSeen >= cutoff)
363
+ .sort((a, b) => b.lastSeen - a.lastSeen)
364
+ .slice(0, 9);
365
+
366
+ sorted.forEach((s, i) => {
367
+ console.log((i+1) + '|' + s.tool + '|' + s.id + '|' + words(s.firstPrompt));
368
+ });
369
+ " 2>/dev/null
370
+ }
371
+
372
+ # Latest Codex session ID for this cwd (fallback when no terminal state exists)
373
+ get_latest_codex_session() {
374
+ node -e "
375
+ const fs = require('fs');
376
+ const path = require('path');
377
+ const dir = '${HOME}/.codex/sessions';
378
+ const cwd = '/home/runner/workspace';
379
+ if (!fs.existsSync(dir)) process.exit(0);
380
+ const walk = (d) => {
381
+ let r = [];
382
+ try {
383
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
384
+ const f = path.join(d, e.name);
385
+ if (e.isDirectory()) r = r.concat(walk(f));
386
+ else if (e.isFile() && e.name.endsWith('.jsonl')) r.push(f);
387
+ }
388
+ } catch(e) {}
389
+ return r;
390
+ };
391
+ let best = null;
392
+ for (const f of walk(dir)) {
393
+ try {
394
+ const first = fs.readFileSync(f, 'utf8').split('\n', 1)[0];
395
+ const meta = JSON.parse(first);
396
+ if (meta.type !== 'session_meta' || !meta.payload) continue;
397
+ if (meta.payload.cwd !== cwd) continue;
398
+ const ts = fs.statSync(f).mtimeMs;
399
+ if (!best || ts > best.ts) best = { id: meta.payload.id, ts };
400
+ } catch(e) {}
401
+ }
402
+ if (best) console.log(best.id);
403
+ " 2>/dev/null
404
+ }
405
+
406
+ # Run a CLI command and update session state on clean exit
407
+ run_tool_with_retry() {
230
408
  local cmd="$1"
231
409
  local session_desc="$2"
410
+ local tool="${3:-claude}"
232
411
 
233
412
  echo ""
234
413
  echo " ${session_desc}..."
235
414
 
236
- # Run Claude
237
415
  eval "$cmd"
238
416
  local exit_code=$?
239
417
 
240
- # Save session state on success
241
418
  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)"
419
+ if [ "$tool" = "codex" ]; then
420
+ local cid=$(get_latest_codex_session)
421
+ [ -n "$cid" ] && save_session_state "$cid" "codex"
422
+ else
423
+ save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)" "claude"
424
+ fi
243
425
  fi
244
426
 
245
427
  return $exit_code
@@ -250,8 +432,12 @@ claude_prompt() {
250
432
  # Only in interactive shells
251
433
  [[ $- != *i* ]] && return 0
252
434
 
253
- # Check if claude exists
254
- if ! command -v claude &>/dev/null; then
435
+ # Need at least one of the tools
436
+ local has_claude=0
437
+ local has_codex=0
438
+ command -v claude &>/dev/null && has_claude=1
439
+ command -v codex &>/dev/null && has_codex=1
440
+ if [ "$has_claude" -eq 0 ] && [ "$has_codex" -eq 0 ]; then
255
441
  return 0
256
442
  fi
257
443
 
@@ -284,69 +470,125 @@ claude_prompt() {
284
470
 
285
471
  # Main menu loop - keeps showing until user chooses shell
286
472
  while true; do
287
- local running=$(count_claude_instances)
288
- local last_session=$(get_terminal_last_session)
473
+ local claude_running=$(count_claude_instances)
474
+ local codex_running=$(count_codex_instances)
475
+ local last_entry=$(get_terminal_last_session)
476
+ local last_tool="${last_entry%%|*}"
477
+ local last_session="${last_entry#*|}"
478
+ [ "$last_tool" = "$last_entry" ] && last_tool=""
289
479
 
290
480
  # Colored command key (shown first)
291
481
  echo ""
292
482
  echo " ┌─────────────────────────────┐"
293
- echo -e " │ \033[95mAt \033[94m~/workspace\033[1;97m\$\033[0;95m prompt:\033[0m │"
483
+ echo -e " │ \033[95mAt \033[1;38;5;33m~/workspace\033[0;97m\$\033[95m prompt:\033[0m │"
294
484
  echo -e " │ \033[96mclaude-menu\033[0m = show menu │"
295
485
  echo -e " │ \033[96mcm\033[0m = menu shortcut │"
296
- echo -e " │ \033[96ml\033[0m = login to claude │"
486
+ echo -e " │ \033[96mj\033[0m = login to claude │"
487
+ echo -e " │ \033[96mk\033[0m = login to codex │"
297
488
  echo " ├─────────────────────────────┤"
298
489
  echo -e " │ \033[1;38;5;208mIn Claude:\033[0m │"
299
490
  echo -e " │ \033[92mCtrl+C x2\033[0m = back to menu │"
300
491
  echo -e " │ \033[92mCtrl+C x3\033[0m = exit to shell │"
301
492
  echo " └─────────────────────────────┘"
302
493
 
494
+ # Recent Sessions (last 24h) - numbered for instant resume
495
+ local recent_24h=$(get_recent_24h_sessions)
496
+ local recent_tools=()
497
+ local recent_ids=()
498
+ if [ -n "$recent_24h" ]; then
499
+ echo ""
500
+ echo " ┌─────────────────────────────┐"
501
+ echo " │ Recent (last 24h) │"
502
+ echo " ├─────────────────────────────┤"
503
+ while IFS='|' read -r num tool id snippet; do
504
+ [ -z "$num" ] && continue
505
+ recent_tools[$num]="$tool"
506
+ recent_ids[$num]="$id"
507
+ if [ "$tool" = "codex" ]; then
508
+ echo -e " │ \033[1;97m[$num]\033[0m \033[1;38;5;208mcdx\033[0m $(printf '%-22s' "${snippet:0:22}")│"
509
+ else
510
+ echo -e " │ \033[1;97m[$num]\033[0m \033[1;38;5;33mcld\033[0m $(printf '%-22s' "${snippet:0:22}")│"
511
+ fi
512
+ done <<< "$recent_24h"
513
+ echo " └─────────────────────────────┘"
514
+ fi
515
+
303
516
  echo ""
304
517
  echo " ┌─────────────────────────────┐"
305
- echo " │ Claude Session Manager │"
518
+ echo " │ DATA Session Manager │"
306
519
  echo " └─────────────────────────────┘"
307
520
 
308
- if [ "$running" -gt 0 ]; then
309
- echo " ($running running)"
521
+ local running_note=""
522
+ [ "$claude_running" -gt 0 ] && running_note="${claude_running} claude"
523
+ if [ "$codex_running" -gt 0 ]; then
524
+ [ -n "$running_note" ] && running_note="${running_note}, "
525
+ running_note="${running_note}${codex_running} codex"
310
526
  fi
527
+ [ -n "$running_note" ] && echo " (${running_note} running)"
311
528
  echo ""
312
529
 
313
530
  # Show options
314
531
  echo " [c] Continue last session"
315
532
  if [ -n "$last_session" ]; then
316
- echo " └─ ${last_session:0:8}..."
533
+ echo " └─ ${last_tool}:${last_session:0:8}..."
317
534
  fi
318
- echo " [r] Resume (pick from list)"
319
- echo " [n] Start new session"
320
- echo " [l] Login to Claude"
535
+ [ -n "$recent_24h" ] && echo " [1-9] Resume numbered above"
536
+ echo " [r] Resume (full list)"
537
+ echo " [n] New Claude session"
538
+ echo " [m] New Codex session"
539
+ echo " [j] Login to Claude"
540
+ echo " [k] Login to Codex"
321
541
  echo " [s] Skip - just shell"
322
542
  echo ""
323
543
 
324
544
  # Read choice with timeout
325
545
  local choice
326
- read -t 60 -n 1 -p " Choice [c/r/n/l/s]: " choice
546
+ read -t 60 -n 1 -p " Choice: " choice
327
547
  echo ""
328
548
 
329
549
  case "$choice" in
550
+ [1-9])
551
+ local sel_tool="${recent_tools[$choice]}"
552
+ local sel_id="${recent_ids[$choice]}"
553
+ if [ -n "$sel_id" ]; then
554
+ if [ "$sel_tool" = "codex" ]; then
555
+ run_tool_with_retry "codex resume '$sel_id'" "Resuming codex session ${sel_id:0:8}" "codex"
556
+ else
557
+ run_tool_with_retry "claude -r '$sel_id' --dangerously-skip-permissions" "Resuming claude session ${sel_id:0:8}" "claude"
558
+ fi
559
+ echo ""
560
+ echo " Exited. Returning to menu..."
561
+ sleep 1
562
+ else
563
+ echo " No session at position $choice."
564
+ sleep 1
565
+ fi
566
+ ;;
330
567
  c|C|"")
331
- # Continue last session (default on Enter or timeout)
568
+ # Continue whichever tool was most recent in this shell
332
569
  if [ -n "$last_session" ]; then
333
- run_claude_with_retry "claude -r '$last_session' --dangerously-skip-permissions" "Resuming session ${last_session:0:8}"
570
+ if [ "$last_tool" = "codex" ]; then
571
+ run_tool_with_retry "codex resume '$last_session'" "Resuming codex session ${last_session:0:8}" "codex"
572
+ else
573
+ run_tool_with_retry "claude -r '$last_session' --dangerously-skip-permissions" "Resuming claude session ${last_session:0:8}" "claude"
574
+ fi
334
575
  else
335
- run_claude_with_retry "claude --dangerously-skip-permissions" "No previous session, starting new"
576
+ run_tool_with_retry "claude --dangerously-skip-permissions" "No previous session, starting new Claude" "claude"
336
577
  fi
337
- # After Claude exits, loop back to menu
338
578
  echo ""
339
- echo " Claude exited. Returning to menu..."
579
+ echo " Exited. Returning to menu..."
340
580
  sleep 1
341
581
  ;;
342
582
  r|R)
343
- # Show session list with full details
583
+ # Combined session list (claude + codex)
344
584
  echo ""
345
585
  echo " Recent Sessions"
346
586
  show_sessions
347
587
 
348
- # Get session IDs for selection
349
- local session_ids=$(get_recent_sessions | grep "^ID|" | cut -d'|' -f2)
588
+ # Parallel arrays of tools and ids (index-aligned to SESSION numbers)
589
+ local data=$(get_recent_sessions)
590
+ local tools_list=$(echo "$data" | grep "^TOOL|" | cut -d'|' -f2)
591
+ local ids_list=$(echo "$data" | grep "^ID|" | cut -d'|' -f2)
350
592
 
351
593
  read -t 60 -p " Enter number (or 'q' to cancel): " session_num
352
594
 
@@ -355,49 +597,60 @@ claude_prompt() {
355
597
  continue
356
598
  fi
357
599
 
358
- local selected_id=$(echo "$session_ids" | sed -n "${session_num}p")
600
+ local selected_tool=$(echo "$tools_list" | sed -n "${session_num}p")
601
+ local selected_id=$(echo "$ids_list" | sed -n "${session_num}p")
359
602
  if [ -n "$selected_id" ]; then
360
- run_claude_with_retry "claude -r '$selected_id' --dangerously-skip-permissions" "Resuming session $selected_id"
603
+ if [ "$selected_tool" = "codex" ]; then
604
+ run_tool_with_retry "codex resume '$selected_id'" "Resuming codex session $selected_id" "codex"
605
+ else
606
+ run_tool_with_retry "claude -r '$selected_id' --dangerously-skip-permissions" "Resuming claude session $selected_id" "claude"
607
+ fi
361
608
  echo ""
362
- echo " Claude exited. Returning to menu..."
609
+ echo " Exited. Returning to menu..."
363
610
  sleep 1
364
611
  else
365
612
  echo " Invalid selection."
366
613
  fi
367
614
  ;;
368
615
  n|N)
369
- # Start new session
370
- run_claude_with_retry "claude --dangerously-skip-permissions" "Starting new Claude session"
616
+ run_tool_with_retry "claude --dangerously-skip-permissions" "Starting new Claude session" "claude"
371
617
  echo ""
372
- echo " Claude exited. Returning to menu..."
618
+ echo " Exited. Returning to menu..."
373
619
  sleep 1
374
620
  ;;
375
- l|L)
376
- # Login to Claude
621
+ m|M)
622
+ run_tool_with_retry "codex" "Starting new Codex session" "codex"
623
+ echo ""
624
+ echo " Exited. Returning to menu..."
625
+ sleep 1
626
+ ;;
627
+ j|J|l|L)
377
628
  echo ""
378
629
  echo " Starting Claude login..."
379
630
  echo ""
380
-
381
- # Clear the auth failed marker so setup script will retry
382
631
  rm -f "${REPLIT_TOOLS}/.auth-refresh-failed" 2>/dev/null
383
-
384
632
  claude /login --dangerously-skip-permissions
385
-
633
+ echo ""
634
+ echo " Login complete. Returning to menu..."
635
+ sleep 1
636
+ ;;
637
+ k|K)
638
+ echo ""
639
+ echo " Starting Codex login..."
640
+ echo ""
641
+ codex login
386
642
  echo ""
387
643
  echo " Login complete. Returning to menu..."
388
644
  sleep 1
389
645
  ;;
390
646
  s|S)
391
- # Skip - just shell
392
647
  echo ""
393
648
  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
649
  return 0
397
650
  ;;
398
651
  *)
399
652
  echo ""
400
- echo " Unknown option. Please choose c, r, n, l, or s."
653
+ echo " Unknown option. Choose 1-9, c, r, n, m, j, k, or s."
401
654
  sleep 1
402
655
  ;;
403
656
  esac
@@ -409,8 +662,12 @@ alias cr='claude -c --dangerously-skip-permissions'
409
662
  alias claude-resume='claude -c --dangerously-skip-permissions'
410
663
  alias claude-pick='claude -r --dangerously-skip-permissions'
411
664
  alias claude-new='claude --dangerously-skip-permissions'
412
- alias l='claude /login --dangerously-skip-permissions'
665
+ alias j='claude /login --dangerously-skip-permissions'
413
666
  alias claude-login='claude /login --dangerously-skip-permissions'
667
+ alias k='codex login'
668
+ alias codex-login='codex login'
669
+ alias codex-new='codex'
670
+ alias codex-resume='codex resume'
414
671
 
415
672
  # Export for manual use
416
673
  export -f get_recent_sessions