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 +2 -1
- package/scripts/claude-session-manager.sh +388 -131
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "replit-tools",
|
|
3
|
-
"version": "1.2.
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
42
|
-
|
|
39
|
+
const historyFile = '${history}';
|
|
40
|
+
const projectsDir = '${projects_dir}';
|
|
41
|
+
const codexSessionsDir = '${codex_sessions_dir}';
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
const sessionData = new Map();
|
|
45
44
|
|
|
46
|
-
|
|
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(
|
|
55
|
-
sessionData.set(
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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
|
-
"
|
|
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{
|
|
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
|
-
#
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
254
|
-
|
|
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
|
|
288
|
-
local
|
|
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[
|
|
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[
|
|
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 " │
|
|
518
|
+
echo " │ DATA Session Manager │"
|
|
306
519
|
echo " └─────────────────────────────┘"
|
|
307
520
|
|
|
308
|
-
|
|
309
|
-
|
|
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 " [
|
|
319
|
-
echo " [
|
|
320
|
-
echo " [
|
|
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
|
|
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
|
|
568
|
+
# Continue whichever tool was most recent in this shell
|
|
332
569
|
if [ -n "$last_session" ]; then
|
|
333
|
-
|
|
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
|
-
|
|
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 "
|
|
579
|
+
echo " Exited. Returning to menu..."
|
|
340
580
|
sleep 1
|
|
341
581
|
;;
|
|
342
582
|
r|R)
|
|
343
|
-
#
|
|
583
|
+
# Combined session list (claude + codex)
|
|
344
584
|
echo ""
|
|
345
585
|
echo " Recent Sessions"
|
|
346
586
|
show_sessions
|
|
347
587
|
|
|
348
|
-
#
|
|
349
|
-
local
|
|
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
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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 "
|
|
618
|
+
echo " Exited. Returning to menu..."
|
|
373
619
|
sleep 1
|
|
374
620
|
;;
|
|
375
|
-
|
|
376
|
-
|
|
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.
|
|
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
|
|
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
|