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 +2 -1
- package/scripts/claude-session-manager.sh +395 -130
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "replit-tools",
|
|
3
|
-
"version": "1.2.
|
|
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
|
-
|
|
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,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
|
|
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 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
|
-
|
|
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
|
-
#
|
|
254
|
-
|
|
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
|
|
288
|
-
local
|
|
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[
|
|
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 " │
|
|
526
|
+
echo " │ DATA Session Manager │"
|
|
306
527
|
echo " └─────────────────────────────┘"
|
|
307
528
|
|
|
308
|
-
|
|
309
|
-
|
|
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 " [
|
|
319
|
-
echo " [
|
|
320
|
-
echo " [
|
|
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
|
|
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
|
|
576
|
+
# Continue whichever tool was most recent in this shell
|
|
332
577
|
if [ -n "$last_session" ]; then
|
|
333
|
-
|
|
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
|
-
|
|
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 "
|
|
587
|
+
echo " Exited. Returning to menu..."
|
|
340
588
|
sleep 1
|
|
341
589
|
;;
|
|
342
590
|
r|R)
|
|
343
|
-
#
|
|
591
|
+
# Combined session list (claude + codex)
|
|
344
592
|
echo ""
|
|
345
593
|
echo " Recent Sessions"
|
|
346
594
|
show_sessions
|
|
347
595
|
|
|
348
|
-
#
|
|
349
|
-
local
|
|
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
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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 "
|
|
626
|
+
echo " Exited. Returning to menu..."
|
|
373
627
|
sleep 1
|
|
374
628
|
;;
|
|
375
|
-
|
|
376
|
-
|
|
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.
|
|
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
|
|
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
|