replit-tools 1.0.0

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/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # replit-claude-persist
2
+
3
+ **Persist Claude Code sessions, authentication, and history across Replit container restarts.**
4
+
5
+ When Replit containers restart, everything outside `/home/runner/workspace/` is wiped - including your Claude conversations, auth tokens, and installed binaries. This tool fixes that.
6
+
7
+ ## Features
8
+
9
+ - **Session Persistence** - Conversations survive container restarts
10
+ - **Interactive Session Picker** - Choose which session to resume on shell start
11
+ - **Multi-Terminal Support** - Each terminal tracks its own session
12
+ - **Auth Persistence** - Keep your Claude authentication working
13
+ - **Binary Caching** - Claude binary persists (faster startup)
14
+ - **Bash History** - Command history survives restarts too
15
+
16
+ ## Installation
17
+
18
+ ### Option 1: npx (easiest)
19
+
20
+ ```bash
21
+ npx replit-claude-persist
22
+ ```
23
+
24
+ ### Option 2: curl
25
+
26
+ ```bash
27
+ curl -fsSL https://raw.githubusercontent.com/stevemoraco/DATAtools/main/install.sh | bash
28
+ ```
29
+
30
+ ### Option 3: Manual
31
+
32
+ ```bash
33
+ npm install -g replit-claude-persist
34
+ replit-claude-persist
35
+ ```
36
+
37
+ ## What You'll See
38
+
39
+ After installation, opening a new shell shows:
40
+
41
+ ```
42
+ ✅ Claude authentication: valid (23h remaining)
43
+ ✅ Claude Code ready: 2.0.71 (Claude Code)
44
+
45
+ ╭─────────────────────────────────────────────────────────╮
46
+ │ Claude Session Manager │
47
+ ╰─────────────────────────────────────────────────────────╯
48
+ (2 Claude instance(s) running in other terminals)
49
+
50
+ [c] Continue last session for this terminal
51
+ └─ b3dcb95c...
52
+ [r] Resume a specific session (pick from list)
53
+ [n] Start new session
54
+ [s] Skip - just give me a shell
55
+
56
+ Choice [c/r/n/s]: _
57
+ ```
58
+
59
+ Press `r` to see detailed session info:
60
+
61
+ ```
62
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
63
+ [1]
64
+ ID: b3dcb95c-cebb-4082-b671-988c8d36578e
65
+ Messages: 237 | Size: 912.1KB
66
+ Active: 2m ago
67
+ Started: 2026-01-18 18:22:12 UTC
68
+ First: "Can you help me fix this bug..."
69
+ Latest: "Thanks, that worked!"
70
+ ```
71
+
72
+ ## How It Works
73
+
74
+ The tool creates symlinks from ephemeral locations to persistent workspace storage:
75
+
76
+ ```
77
+ ~/.claude → /workspace/.claude-persistent/
78
+ ~/.codex → /workspace/.codex-persistent/
79
+ ~/.local/bin/ → /workspace/.local/share/claude/versions/
80
+ ```
81
+
82
+ On every shell start, `.config/bashrc` ensures symlinks exist and shows the session picker.
83
+
84
+ ## Commands
85
+
86
+ | Command | Description |
87
+ |---------|-------------|
88
+ | `cr` | Continue last session |
89
+ | `claude-menu` | Show session picker again |
90
+ | `claude-new` | Start fresh session |
91
+ | `claude-pick` | Claude's built-in picker |
92
+
93
+ ## Configuration
94
+
95
+ ### Disable the menu
96
+
97
+ ```bash
98
+ export CLAUDE_NO_PROMPT=true
99
+ ```
100
+
101
+ Add to `.config/bashrc` to make permanent.
102
+
103
+ ### Fix auth permanently
104
+
105
+ ```bash
106
+ claude setup-token
107
+ ```
108
+
109
+ Creates a long-lived token that doesn't expire.
110
+
111
+ ## Files Created
112
+
113
+ ```
114
+ workspace/
115
+ ├── .claude-persistent/ # Conversations, credentials
116
+ ├── .codex-persistent/ # Codex CLI data
117
+ ├── .claude-sessions/ # Per-terminal session tracking
118
+ ├── .local/share/claude/ # Claude binary versions
119
+ ├── .persistent-home/ # Bash history
120
+ ├── .config/bashrc # Shell startup config
121
+ └── scripts/
122
+ ├── setup-claude-code.sh
123
+ └── claude-session-manager.sh
124
+ ```
125
+
126
+ ## Troubleshooting
127
+
128
+ ### Menu not appearing
129
+
130
+ ```bash
131
+ source /home/runner/workspace/.config/bashrc
132
+ ```
133
+
134
+ ### Auth expired
135
+
136
+ ```bash
137
+ claude login
138
+ # Or for permanent fix:
139
+ claude setup-token
140
+ ```
141
+
142
+ ### Symlinks broken
143
+
144
+ ```bash
145
+ source /home/runner/workspace/scripts/setup-claude-code.sh
146
+ ```
147
+
148
+ ## License
149
+
150
+ MIT
package/index.js ADDED
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync, spawn } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const WORKSPACE = '/home/runner/workspace';
8
+
9
+ // Check if we're on Replit
10
+ if (!fs.existsSync(WORKSPACE)) {
11
+ console.error('❌ This tool must be run on Replit');
12
+ console.error(' /home/runner/workspace not found');
13
+ process.exit(1);
14
+ }
15
+
16
+ console.log('');
17
+ console.log('╭─────────────────────────────────────────────────────────╮');
18
+ console.log('│ Replit Claude Persistence Installer │');
19
+ console.log('╰─────────────────────────────────────────────────────────╯');
20
+ console.log('');
21
+
22
+ // Create directories
23
+ const dirs = [
24
+ '.claude-persistent',
25
+ '.codex-persistent',
26
+ '.claude-sessions',
27
+ '.local/share/claude/versions',
28
+ '.persistent-home',
29
+ '.config',
30
+ 'scripts',
31
+ 'logs'
32
+ ];
33
+
34
+ console.log('📁 Creating directories...');
35
+ dirs.forEach(dir => {
36
+ const fullPath = path.join(WORKSPACE, dir);
37
+ if (!fs.existsSync(fullPath)) {
38
+ fs.mkdirSync(fullPath, { recursive: true });
39
+ }
40
+ });
41
+
42
+ // Copy scripts from the package
43
+ const scriptsDir = path.join(__dirname, 'scripts');
44
+ const targetScriptsDir = path.join(WORKSPACE, 'scripts');
45
+
46
+ console.log('📝 Installing scripts...');
47
+
48
+ // Read and write each script
49
+ const scripts = ['setup-claude-code.sh', 'claude-session-manager.sh'];
50
+ scripts.forEach(script => {
51
+ const srcPath = path.join(scriptsDir, script);
52
+ const destPath = path.join(targetScriptsDir, script);
53
+
54
+ if (fs.existsSync(srcPath)) {
55
+ fs.copyFileSync(srcPath, destPath);
56
+ fs.chmodSync(destPath, '755');
57
+ }
58
+ });
59
+
60
+ // Create/update .config/bashrc
61
+ console.log('📝 Creating .config/bashrc...');
62
+ const bashrcContent = `#!/bin/bash
63
+ # Replit Claude Persistence - Auto-generated bashrc
64
+
65
+ # Claude Code Setup
66
+ SETUP_SCRIPT="/home/runner/workspace/scripts/setup-claude-code.sh"
67
+ [ -f "\${SETUP_SCRIPT}" ] && source "\${SETUP_SCRIPT}"
68
+
69
+ # Codex Persistence
70
+ CODEX_PERSISTENT="/home/runner/workspace/.codex-persistent"
71
+ mkdir -p "\${CODEX_PERSISTENT}"
72
+ [ ! -L "\${HOME}/.codex" ] && ln -sf "\${CODEX_PERSISTENT}" "\${HOME}/.codex"
73
+
74
+ # Bash History Persistence
75
+ PERSISTENT_HOME="/home/runner/workspace/.persistent-home"
76
+ mkdir -p "\${PERSISTENT_HOME}"
77
+ export HISTFILE="\${PERSISTENT_HOME}/.bash_history"
78
+ export HISTSIZE=10000
79
+ export HISTFILESIZE=20000
80
+ export HISTCONTROL=ignoredups
81
+ [ -f "\${HISTFILE}" ] && history -r "\${HISTFILE}"
82
+
83
+ # Session Manager (interactive menu)
84
+ SESSION_MANAGER="/home/runner/workspace/scripts/claude-session-manager.sh"
85
+ [ -f "\${SESSION_MANAGER}" ] && source "\${SESSION_MANAGER}"
86
+
87
+ # Aliases
88
+ alias cr='claude -c --dangerously-skip-permissions'
89
+ alias claude-resume='claude -c --dangerously-skip-permissions'
90
+ alias claude-pick='claude -r --dangerously-skip-permissions'
91
+ `;
92
+
93
+ fs.writeFileSync(path.join(WORKSPACE, '.config/bashrc'), bashrcContent);
94
+
95
+ // Update .replit
96
+ console.log('📝 Updating .replit configuration...');
97
+ const replitPath = path.join(WORKSPACE, '.replit');
98
+ const onBootLine = 'onBoot = "source /home/runner/workspace/scripts/setup-claude-code.sh 2>/dev/null || true"';
99
+
100
+ if (fs.existsSync(replitPath)) {
101
+ let content = fs.readFileSync(replitPath, 'utf8');
102
+ if (!content.includes('setup-claude-code.sh')) {
103
+ content += '\n\n# Claude persistence (added by installer)\n' + onBootLine + '\n';
104
+ fs.writeFileSync(replitPath, content);
105
+ }
106
+ } else {
107
+ fs.writeFileSync(replitPath, '# Claude persistence\n' + onBootLine + '\n');
108
+ }
109
+
110
+ console.log('');
111
+ console.log('✅ Installation complete!');
112
+ console.log('');
113
+ console.log('What happens now:');
114
+ console.log(' • New shells will show the Claude session picker');
115
+ console.log(' • Your conversations persist across container restarts');
116
+ console.log(' • Claude binary is cached (faster startup)');
117
+ console.log(' • Bash history is preserved');
118
+ console.log('');
119
+ console.log('To test, open a new shell or run:');
120
+ console.log(' source ~/.config/bashrc');
121
+ console.log('');
122
+ console.log('Options:');
123
+ console.log(" Press 'c' - Continue last session");
124
+ console.log(" Press 'r' - Pick from session list");
125
+ console.log(" Press 'n' - New session");
126
+ console.log(" Press 's' - Skip (just a shell)");
127
+ console.log('');
128
+ console.log('To disable the menu: export CLAUDE_NO_PROMPT=true');
129
+ console.log('');
package/install.sh ADDED
@@ -0,0 +1,419 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # Replit Claude Persistence Installer
4
+ # =============================================================================
5
+ # One-line install:
6
+ # curl -fsSL https://raw.githubusercontent.com/YOUR_USERNAME/replit-claude-persist/main/install.sh | bash
7
+ #
8
+ # Or with npx:
9
+ # npx replit-claude-persist
10
+ # =============================================================================
11
+
12
+ set -e
13
+
14
+ WORKSPACE="/home/runner/workspace"
15
+ GREEN='\033[0;32m'
16
+ YELLOW='\033[1;33m'
17
+ RED='\033[0;31m'
18
+ NC='\033[0m' # No Color
19
+
20
+ echo ""
21
+ echo "╭─────────────────────────────────────────────────────────╮"
22
+ echo "│ Replit Claude Persistence Installer │"
23
+ echo "╰─────────────────────────────────────────────────────────╯"
24
+ echo ""
25
+
26
+ # Check we're on Replit
27
+ if [ ! -d "/home/runner/workspace" ]; then
28
+ echo -e "${RED}ERROR: This script must be run on Replit${NC}"
29
+ echo " /home/runner/workspace not found"
30
+ exit 1
31
+ fi
32
+
33
+ cd "$WORKSPACE"
34
+
35
+ echo "📁 Creating directories..."
36
+ mkdir -p .claude-persistent
37
+ mkdir -p .codex-persistent
38
+ mkdir -p .claude-sessions
39
+ mkdir -p .local/share/claude/versions
40
+ mkdir -p .persistent-home
41
+ mkdir -p .config
42
+ mkdir -p scripts
43
+ mkdir -p logs
44
+
45
+ echo "📝 Installing scripts..."
46
+
47
+ # setup-claude-code.sh
48
+ cat > scripts/setup-claude-code.sh << 'SCRIPT_EOF'
49
+ #!/bin/bash
50
+ # Claude Code Setup Script for Replit
51
+ set -e
52
+
53
+ WORKSPACE="/home/runner/workspace"
54
+ CLAUDE_PERSISTENT="${WORKSPACE}/.claude-persistent"
55
+ CLAUDE_LOCAL_SHARE="${WORKSPACE}/.local/share/claude"
56
+ CLAUDE_VERSIONS="${CLAUDE_LOCAL_SHARE}/versions"
57
+ CLAUDE_SYMLINK="${HOME}/.claude"
58
+ LOCAL_BIN="${HOME}/.local/bin"
59
+ LOCAL_SHARE_CLAUDE="${HOME}/.local/share/claude"
60
+
61
+ log() {
62
+ if [[ $- == *i* ]]; then
63
+ echo "$1"
64
+ fi
65
+ }
66
+
67
+ mkdir -p "${CLAUDE_PERSISTENT}"
68
+ mkdir -p "${CLAUDE_VERSIONS}"
69
+ mkdir -p "${LOCAL_BIN}"
70
+ mkdir -p "${HOME}/.local/share"
71
+
72
+ # Symlink ~/.claude
73
+ if [ ! -L "${CLAUDE_SYMLINK}" ] || [ "$(readlink -f "${CLAUDE_SYMLINK}")" != "${CLAUDE_PERSISTENT}" ]; then
74
+ rm -rf "${CLAUDE_SYMLINK}" 2>/dev/null || true
75
+ ln -sf "${CLAUDE_PERSISTENT}" "${CLAUDE_SYMLINK}"
76
+ log "✅ Claude history symlink: ~/.claude -> ${CLAUDE_PERSISTENT}"
77
+ fi
78
+
79
+ # Symlink ~/.local/share/claude
80
+ if [ ! -L "${LOCAL_SHARE_CLAUDE}" ] || [ "$(readlink -f "${LOCAL_SHARE_CLAUDE}")" != "${CLAUDE_LOCAL_SHARE}" ]; then
81
+ rm -rf "${LOCAL_SHARE_CLAUDE}" 2>/dev/null || true
82
+ ln -sf "${CLAUDE_LOCAL_SHARE}" "${LOCAL_SHARE_CLAUDE}"
83
+ fi
84
+
85
+ # Find and symlink binary
86
+ LATEST_VERSION=""
87
+ if [ -d "${CLAUDE_VERSIONS}" ]; then
88
+ LATEST_VERSION=$(ls -1 "${CLAUDE_VERSIONS}" 2>/dev/null | sort -V | tail -n1)
89
+ fi
90
+
91
+ if [ -n "${LATEST_VERSION}" ] && [ -f "${CLAUDE_VERSIONS}/${LATEST_VERSION}" ]; then
92
+ CLAUDE_BINARY="${CLAUDE_VERSIONS}/${LATEST_VERSION}"
93
+ if [ ! -L "${LOCAL_BIN}/claude" ] || [ "$(readlink -f "${LOCAL_BIN}/claude")" != "${CLAUDE_BINARY}" ]; then
94
+ rm -f "${LOCAL_BIN}/claude" 2>/dev/null || true
95
+ ln -sf "${CLAUDE_BINARY}" "${LOCAL_BIN}/claude"
96
+ log "✅ Claude binary symlink: ~/.local/bin/claude -> ${CLAUDE_BINARY}"
97
+ fi
98
+ else
99
+ log "⚠️ Claude Code not found, installing..."
100
+ if command -v npm &> /dev/null; then
101
+ npm install -g @anthropic-ai/claude-code 2>/dev/null || true
102
+ if command -v claude &> /dev/null; then
103
+ INSTALLED_PATH=$(which claude)
104
+ if [ -f "${INSTALLED_PATH}" ] && [ ! -L "${INSTALLED_PATH}" ]; then
105
+ VERSION=$(claude --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1)
106
+ if [ -n "${VERSION}" ]; then
107
+ cp "${INSTALLED_PATH}" "${CLAUDE_VERSIONS}/${VERSION}"
108
+ chmod +x "${CLAUDE_VERSIONS}/${VERSION}"
109
+ ln -sf "${CLAUDE_VERSIONS}/${VERSION}" "${LOCAL_BIN}/claude"
110
+ log "✅ Claude Code ${VERSION} installed"
111
+ fi
112
+ fi
113
+ fi
114
+ fi
115
+ fi
116
+
117
+ # Ensure PATH
118
+ if [[ ":$PATH:" != *":${LOCAL_BIN}:"* ]]; then
119
+ export PATH="${LOCAL_BIN}:$PATH"
120
+ fi
121
+
122
+ # Check auth
123
+ CREDENTIALS_FILE="${CLAUDE_PERSISTENT}/.credentials.json"
124
+ if [ -f "${CREDENTIALS_FILE}" ]; then
125
+ if command -v node &> /dev/null; then
126
+ AUTH_INFO=$(node -e "
127
+ try {
128
+ const creds = require('${CREDENTIALS_FILE}');
129
+ const oauth = creds.claudeAiOauth;
130
+ const apiKey = creds.primaryApiKey;
131
+ if (apiKey) console.log('apikey');
132
+ else if (oauth && oauth.expiresAt) console.log(oauth.expiresAt);
133
+ } catch(e) {}
134
+ " 2>/dev/null)
135
+
136
+ if [ "${AUTH_INFO}" = "apikey" ]; then
137
+ log "✅ Claude authentication: long-lived token (no expiration)"
138
+ elif [ -n "${AUTH_INFO}" ]; then
139
+ CURRENT_TIME=$(node -e "console.log(Date.now())" 2>/dev/null)
140
+ if [ -n "${CURRENT_TIME}" ] && [ "${AUTH_INFO}" -gt "${CURRENT_TIME}" ]; then
141
+ HOURS_LEFT=$(node -e "console.log(Math.floor((${AUTH_INFO} - ${CURRENT_TIME}) / 1000 / 60 / 60))" 2>/dev/null)
142
+ if [ "${HOURS_LEFT}" -lt 2 ]; then
143
+ log "⚠️ Claude authentication: expires in ${HOURS_LEFT}h"
144
+ else
145
+ log "✅ Claude authentication: valid (${HOURS_LEFT}h remaining)"
146
+ fi
147
+ else
148
+ log "⚠️ Claude authentication: expired, run 'claude login'"
149
+ fi
150
+ fi
151
+ fi
152
+ else
153
+ log "⚠️ No Claude credentials. Run 'claude login'"
154
+ fi
155
+
156
+ if [[ $- == *i* ]] && [ -n "${LATEST_VERSION}" ]; then
157
+ if command -v claude &> /dev/null; then
158
+ CLAUDE_VERSION=$(claude --version 2>/dev/null | head -1)
159
+ log "✅ Claude Code ready: ${CLAUDE_VERSION}"
160
+ fi
161
+ fi
162
+ SCRIPT_EOF
163
+
164
+ # claude-session-manager.sh
165
+ cat > scripts/claude-session-manager.sh << 'SCRIPT_EOF'
166
+ #!/bin/bash
167
+ # Claude Session Manager - Interactive Multi-Terminal Support
168
+
169
+ WORKSPACE="/home/runner/workspace"
170
+ SESSIONS_DIR="${WORKSPACE}/.claude-sessions"
171
+ LOCK_DIR="/tmp/.claude-locks"
172
+
173
+ mkdir -p "${SESSIONS_DIR}" "${LOCK_DIR}" 2>/dev/null
174
+
175
+ get_terminal_id() {
176
+ local tty_name=$(tty 2>/dev/null | sed 's|/dev/||' | tr '/' '-')
177
+ if [ -n "$tty_name" ] && [ "$tty_name" != "not" ]; then
178
+ echo "$tty_name"
179
+ else
180
+ echo "shell-$$"
181
+ fi
182
+ }
183
+
184
+ TERMINAL_ID=$(get_terminal_id)
185
+ STATE_FILE="${SESSIONS_DIR}/${TERMINAL_ID}.json"
186
+
187
+ get_recent_sessions() {
188
+ local history="${HOME}/.claude/history.jsonl"
189
+ local projects_dir="${HOME}/.claude/projects/-home-runner-workspace"
190
+
191
+ if [ -f "${history}" ]; then
192
+ node -e "
193
+ const fs = require('fs');
194
+ const path = require('path');
195
+ const historyFile = '${history}';
196
+ const projectsDir = '${projects_dir}';
197
+ const sessionData = new Map();
198
+ const lines = fs.readFileSync(historyFile, 'utf8').trim().split('\n');
199
+
200
+ for (const line of lines) {
201
+ try {
202
+ const j = JSON.parse(line);
203
+ if (!j.sessionId) continue;
204
+ if (!sessionData.has(j.sessionId)) {
205
+ sessionData.set(j.sessionId, {
206
+ id: j.sessionId, firstSeen: j.timestamp, lastSeen: j.timestamp,
207
+ firstPrompt: j.display || '', lastPrompt: j.display || '',
208
+ messageCount: 0, project: j.project || ''
209
+ });
210
+ }
211
+ const data = sessionData.get(j.sessionId);
212
+ if (j.timestamp < data.firstSeen) { data.firstSeen = j.timestamp; data.firstPrompt = j.display || data.firstPrompt; }
213
+ if (j.timestamp > data.lastSeen) { data.lastSeen = j.timestamp; data.lastPrompt = j.display || data.lastPrompt; }
214
+ } catch(e) {}
215
+ }
216
+
217
+ for (const [id, data] of sessionData) {
218
+ const jsonlPath = path.join(projectsDir, id + '.jsonl');
219
+ if (fs.existsSync(jsonlPath)) {
220
+ try {
221
+ const stat = fs.statSync(jsonlPath);
222
+ data.fileSize = stat.size;
223
+ data.messageCount = fs.readFileSync(jsonlPath, 'utf8').trim().split('\n').filter(l => l.trim()).length;
224
+ } catch(e) {}
225
+ }
226
+ }
227
+
228
+ const sorted = Array.from(sessionData.values()).sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0)).slice(0, 10);
229
+ sorted.forEach((s, i) => {
230
+ const formatTime = (ts) => { if (!ts) return 'unknown'; const d = new Date(ts); return d.toISOString().replace('T', ' ').substring(0, 19) + ' UTC'; };
231
+ const timeAgo = (ts) => { if (!ts) return ''; const mins = Math.round((Date.now() - ts) / 1000 / 60); if (mins < 60) return mins + 'm ago'; if (mins < 1440) return Math.round(mins/60) + 'h ago'; return Math.round(mins/1440) + 'd ago'; };
232
+ const sizeStr = (bytes) => { if (!bytes) return '0B'; if (bytes < 1024) return bytes + 'B'; if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + 'KB'; return (bytes/1024/1024).toFixed(1) + 'MB'; };
233
+ console.log('SESSION|' + (i+1));
234
+ console.log('ID|' + s.id);
235
+ console.log('MESSAGES|' + (s.messageCount || '?'));
236
+ console.log('SIZE|' + sizeStr(s.fileSize));
237
+ console.log('LAST_ACTIVE|' + timeAgo(s.lastSeen));
238
+ console.log('STARTED|' + formatTime(s.firstSeen));
239
+ console.log('FIRST_PROMPT|' + (s.firstPrompt || '').substring(0, 80).replace(/\n/g, ' ').trim());
240
+ console.log('LAST_PROMPT|' + (s.lastPrompt || '').substring(0, 80).replace(/\n/g, ' ').trim());
241
+ console.log('---');
242
+ });
243
+ " 2>/dev/null
244
+ fi
245
+ }
246
+
247
+ show_sessions() {
248
+ local data=$(get_recent_sessions)
249
+ [ -z "$data" ] && echo " No sessions found." && return
250
+ echo "$data" | while IFS='|' read -r key value; do
251
+ case "$key" in
252
+ SESSION) echo ""; echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; echo " [$value]" ;;
253
+ ID) echo " ID: $value" ;;
254
+ MESSAGES) printf " Messages: %s" "$value" ;;
255
+ SIZE) printf " | Size: %s\n" "$value" ;;
256
+ LAST_ACTIVE) echo " Active: $value" ;;
257
+ STARTED) echo " Started: $value" ;;
258
+ FIRST_PROMPT) [ -n "$value" ] && echo " First: \"$value\"" ;;
259
+ LAST_PROMPT) [ -n "$value" ] && echo " Latest: \"$value\"" ;;
260
+ esac
261
+ done
262
+ echo ""
263
+ }
264
+
265
+ count_claude_instances() { pgrep -x "claude" 2>/dev/null | wc -l; }
266
+
267
+ save_session_state() {
268
+ local session_id="$1"
269
+ local flags="${2:---dangerously-skip-permissions}"
270
+ mkdir -p "${SESSIONS_DIR}"
271
+ cat > "${STATE_FILE}" << EOF
272
+ {"sessionId": "${session_id}", "flags": "${flags}", "terminalId": "${TERMINAL_ID}", "timestamp": $(date +%s)}
273
+ EOF
274
+ }
275
+
276
+ get_terminal_last_session() {
277
+ [ -f "${STATE_FILE}" ] && node -e "try{console.log(require('${STATE_FILE}').sessionId||'')}catch(e){}" 2>/dev/null
278
+ }
279
+
280
+ claude_prompt() {
281
+ [[ $- != *i* ]] && return 0
282
+ command -v claude &>/dev/null || return 0
283
+
284
+ local running=$(count_claude_instances)
285
+ local last_session=$(get_terminal_last_session)
286
+
287
+ echo ""
288
+ echo "╭─────────────────────────────────────────────────────────╮"
289
+ echo "│ Claude Session Manager │"
290
+ echo "╰─────────────────────────────────────────────────────────╯"
291
+ [ "$running" -gt 0 ] && echo " ($running Claude instance(s) running in other terminals)"
292
+ echo ""
293
+ echo " [c] Continue last session for this terminal"
294
+ [ -n "$last_session" ] && echo " └─ ${last_session:0:8}..."
295
+ echo " [r] Resume a specific session (pick from list)"
296
+ echo " [n] Start new session"
297
+ echo " [s] Skip - just give me a shell"
298
+ echo ""
299
+
300
+ local choice
301
+ read -t 30 -n 1 -p " Choice [c/r/n/s]: " choice
302
+ echo ""
303
+
304
+ case "$choice" in
305
+ c|C|"")
306
+ if [ -n "$last_session" ]; then
307
+ echo ""; echo " Resuming session ${last_session:0:8}..."
308
+ claude -r "$last_session" --dangerously-skip-permissions
309
+ save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)"
310
+ else
311
+ echo ""; echo " No previous session, starting new..."
312
+ claude --dangerously-skip-permissions
313
+ save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)"
314
+ fi ;;
315
+ r|R)
316
+ echo ""; echo " Recent Sessions"; show_sessions
317
+ local session_ids=$(get_recent_sessions | grep "^ID|" | cut -d'|' -f2)
318
+ read -p " Enter number (or 'q' to cancel): " session_num
319
+ [ "$session_num" = "q" ] || [ -z "$session_num" ] && echo " Cancelled." && return 0
320
+ local selected_id=$(echo "$session_ids" | sed -n "${session_num}p")
321
+ if [ -n "$selected_id" ]; then
322
+ echo ""; echo " Resuming session: $selected_id"
323
+ claude -r "$selected_id" --dangerously-skip-permissions
324
+ save_session_state "$selected_id"
325
+ else echo " Invalid selection."; fi ;;
326
+ n|N)
327
+ echo ""; echo " Starting new Claude session..."
328
+ claude --dangerously-skip-permissions
329
+ save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)" ;;
330
+ s|S) echo ""; echo " Okay, just a shell. Type 'claude' or 'cr' when ready." ;;
331
+ *) echo ""; echo " Unknown option. Type 'claude' to start manually." ;;
332
+ esac
333
+ }
334
+
335
+ alias cr='claude -c --dangerously-skip-permissions'
336
+ alias claude-resume='claude -c --dangerously-skip-permissions'
337
+ alias claude-pick='claude -r --dangerously-skip-permissions'
338
+ alias claude-new='claude --dangerously-skip-permissions'
339
+ alias claude-menu='claude_prompt'
340
+
341
+ export -f get_recent_sessions save_session_state
342
+ export TERMINAL_ID
343
+
344
+ [ "${CLAUDE_NO_PROMPT}" != "true" ] && claude_prompt
345
+ SCRIPT_EOF
346
+
347
+ chmod +x scripts/setup-claude-code.sh
348
+ chmod +x scripts/claude-session-manager.sh
349
+
350
+ # Create .config/bashrc
351
+ echo "📝 Creating .config/bashrc..."
352
+ cat > .config/bashrc << 'BASHRC_EOF'
353
+ #!/bin/bash
354
+ # Replit Claude Persistence - Auto-generated bashrc
355
+
356
+ # Claude Code Setup
357
+ SETUP_SCRIPT="/home/runner/workspace/scripts/setup-claude-code.sh"
358
+ [ -f "${SETUP_SCRIPT}" ] && source "${SETUP_SCRIPT}"
359
+
360
+ # Codex Persistence
361
+ CODEX_PERSISTENT="/home/runner/workspace/.codex-persistent"
362
+ mkdir -p "${CODEX_PERSISTENT}"
363
+ [ ! -L "${HOME}/.codex" ] && ln -sf "${CODEX_PERSISTENT}" "${HOME}/.codex"
364
+
365
+ # Bash History Persistence
366
+ PERSISTENT_HOME="/home/runner/workspace/.persistent-home"
367
+ mkdir -p "${PERSISTENT_HOME}"
368
+ export HISTFILE="${PERSISTENT_HOME}/.bash_history"
369
+ export HISTSIZE=10000
370
+ export HISTFILESIZE=20000
371
+ export HISTCONTROL=ignoredups
372
+ [ -f "${HISTFILE}" ] && history -r "${HISTFILE}"
373
+
374
+ # Session Manager (interactive menu)
375
+ SESSION_MANAGER="/home/runner/workspace/scripts/claude-session-manager.sh"
376
+ [ -f "${SESSION_MANAGER}" ] && source "${SESSION_MANAGER}"
377
+
378
+ # Aliases
379
+ alias cr='claude -c --dangerously-skip-permissions'
380
+ alias claude-resume='claude -c --dangerously-skip-permissions'
381
+ alias claude-pick='claude -r --dangerously-skip-permissions'
382
+ BASHRC_EOF
383
+
384
+ # Update .replit if it exists
385
+ echo "📝 Updating .replit configuration..."
386
+ if [ -f .replit ]; then
387
+ # Check if onBoot already exists
388
+ if ! grep -q "setup-claude-code.sh" .replit; then
389
+ echo "" >> .replit
390
+ echo '# Claude persistence (added by installer)' >> .replit
391
+ echo 'onBoot = "source /home/runner/workspace/scripts/setup-claude-code.sh 2>/dev/null || true"' >> .replit
392
+ fi
393
+ else
394
+ cat > .replit << 'REPLIT_EOF'
395
+ # Claude persistence
396
+ onBoot = "source /home/runner/workspace/scripts/setup-claude-code.sh 2>/dev/null || true"
397
+ REPLIT_EOF
398
+ fi
399
+
400
+ echo ""
401
+ echo -e "${GREEN}✅ Installation complete!${NC}"
402
+ echo ""
403
+ echo "What happens now:"
404
+ echo " • New shells will show the Claude session picker"
405
+ echo " • Your conversations persist across container restarts"
406
+ echo " • Claude binary is cached (faster startup)"
407
+ echo " • Bash history is preserved"
408
+ echo ""
409
+ echo "To test, open a new shell or run:"
410
+ echo " source ~/.config/bashrc"
411
+ echo ""
412
+ echo "Options:"
413
+ echo " Press 'c' - Continue last session"
414
+ echo " Press 'r' - Pick from session list"
415
+ echo " Press 'n' - New session"
416
+ echo " Press 's' - Skip (just a shell)"
417
+ echo ""
418
+ echo "To disable the menu: export CLAUDE_NO_PROMPT=true"
419
+ echo ""
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "replit-tools",
3
+ "version": "1.0.0",
4
+ "description": "Persist Claude Code sessions, auth, and history across Replit container restarts",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "replit-tools": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "postinstall": "node index.js"
11
+ },
12
+ "keywords": [
13
+ "replit",
14
+ "claude",
15
+ "claude-code",
16
+ "persistence",
17
+ "session",
18
+ "cli"
19
+ ],
20
+ "author": "stevemoraco",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/stevemoraco/DATAtools"
25
+ },
26
+ "homepage": "https://github.com/stevemoraco/DATAtools#readme",
27
+ "engines": {
28
+ "node": ">=16.0.0"
29
+ }
30
+ }
@@ -0,0 +1,344 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # Claude Session Manager - Interactive Multi-Terminal Support
4
+ # =============================================================================
5
+ # Prompts user to choose: resume a session, start new, or skip.
6
+ # Supports multiple terminals with independent session tracking.
7
+ # =============================================================================
8
+
9
+ WORKSPACE="/home/runner/workspace"
10
+ SESSIONS_DIR="${WORKSPACE}/.claude-sessions"
11
+ LOCK_DIR="/tmp/.claude-locks"
12
+
13
+ mkdir -p "${SESSIONS_DIR}" "${LOCK_DIR}" 2>/dev/null
14
+
15
+ # Get terminal identifier
16
+ get_terminal_id() {
17
+ local tty_name=$(tty 2>/dev/null | sed 's|/dev/||' | tr '/' '-')
18
+ if [ -n "$tty_name" ] && [ "$tty_name" != "not" ]; then
19
+ echo "$tty_name"
20
+ else
21
+ echo "shell-$$"
22
+ fi
23
+ }
24
+
25
+ TERMINAL_ID=$(get_terminal_id)
26
+ STATE_FILE="${SESSIONS_DIR}/${TERMINAL_ID}.json"
27
+
28
+ # Get recent sessions with full details
29
+ get_recent_sessions() {
30
+ local history="${HOME}/.claude/history.jsonl"
31
+ local projects_dir="${HOME}/.claude/projects/-home-runner-workspace"
32
+
33
+ if [ -f "${history}" ]; then
34
+ # Collect all session data with full metadata
35
+ node -e "
36
+ const fs = require('fs');
37
+ const path = require('path');
38
+ const readline = require('readline');
39
+
40
+ const historyFile = '${history}';
41
+ const projectsDir = '${projects_dir}';
42
+
43
+ const sessionData = new Map();
44
+
45
+ // Read history to get session metadata
46
+ const lines = fs.readFileSync(historyFile, 'utf8').trim().split('\n');
47
+
48
+ for (const line of lines) {
49
+ try {
50
+ const j = JSON.parse(line);
51
+ if (!j.sessionId) continue;
52
+
53
+ if (!sessionData.has(j.sessionId)) {
54
+ sessionData.set(j.sessionId, {
55
+ id: j.sessionId,
56
+ firstSeen: j.timestamp,
57
+ lastSeen: j.timestamp,
58
+ firstPrompt: j.display || '',
59
+ lastPrompt: j.display || '',
60
+ messageCount: 0,
61
+ project: j.project || ''
62
+ });
63
+ }
64
+
65
+ const data = sessionData.get(j.sessionId);
66
+ if (j.timestamp < data.firstSeen) {
67
+ data.firstSeen = j.timestamp;
68
+ data.firstPrompt = j.display || data.firstPrompt;
69
+ }
70
+ if (j.timestamp > data.lastSeen) {
71
+ data.lastSeen = j.timestamp;
72
+ data.lastPrompt = j.display || data.lastPrompt;
73
+ }
74
+ } catch(e) {}
75
+ }
76
+
77
+ // Enrich with .jsonl file data (message counts, file sizes)
78
+ for (const [id, data] of sessionData) {
79
+ const jsonlPath = path.join(projectsDir, id + '.jsonl');
80
+ const agentPath = path.join(projectsDir, 'agent-' + id.substring(0,7) + '.jsonl');
81
+
82
+ let filePath = null;
83
+ let fileSize = 0;
84
+
85
+ if (fs.existsSync(jsonlPath)) {
86
+ filePath = jsonlPath;
87
+ } else if (fs.existsSync(agentPath)) {
88
+ filePath = agentPath;
89
+ }
90
+
91
+ if (filePath) {
92
+ try {
93
+ const stat = fs.statSync(filePath);
94
+ fileSize = stat.size;
95
+ const content = fs.readFileSync(filePath, 'utf8');
96
+ const msgLines = content.trim().split('\n').filter(l => l.trim());
97
+ data.messageCount = msgLines.length;
98
+ data.fileSize = fileSize;
99
+ data.filePath = filePath;
100
+ } catch(e) {}
101
+ }
102
+ }
103
+
104
+ // Sort by lastSeen descending and output
105
+ const sorted = Array.from(sessionData.values())
106
+ .sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0))
107
+ .slice(0, 10);
108
+
109
+ sorted.forEach((s, i) => {
110
+ const formatTime = (ts) => {
111
+ if (!ts) return 'unknown';
112
+ const d = new Date(ts);
113
+ const utc = d.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
114
+ // MST is UTC-7
115
+ const mst = new Date(ts - 7*60*60*1000).toISOString().replace('T', ' ').substring(0, 19) + ' MST';
116
+ return utc + ' / ' + mst;
117
+ };
118
+
119
+ const timeAgo = (ts) => {
120
+ if (!ts) return '';
121
+ const mins = Math.round((Date.now() - ts) / 1000 / 60);
122
+ if (mins < 60) return mins + 'm ago';
123
+ if (mins < 1440) return Math.round(mins/60) + 'h ago';
124
+ return Math.round(mins/1440) + 'd ago';
125
+ };
126
+
127
+ const sizeStr = (bytes) => {
128
+ if (!bytes) return '0B';
129
+ if (bytes < 1024) return bytes + 'B';
130
+ if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + 'KB';
131
+ return (bytes/1024/1024).toFixed(1) + 'MB';
132
+ };
133
+
134
+ console.log('SESSION|' + (i+1));
135
+ console.log('ID|' + s.id);
136
+ console.log('MESSAGES|' + (s.messageCount || '?'));
137
+ console.log('SIZE|' + sizeStr(s.fileSize));
138
+ console.log('LAST_ACTIVE|' + timeAgo(s.lastSeen));
139
+ console.log('STARTED|' + formatTime(s.firstSeen));
140
+ console.log('LAST_SEEN|' + formatTime(s.lastSeen));
141
+ console.log('FIRST_PROMPT|' + (s.firstPrompt || '').substring(0, 80).replace(/\\n/g, ' ').trim());
142
+ console.log('LAST_PROMPT|' + (s.lastPrompt || '').substring(0, 80).replace(/\\n/g, ' ').trim());
143
+ console.log('---');
144
+ });
145
+ " 2>/dev/null
146
+ fi
147
+ }
148
+
149
+ # Display formatted session list
150
+ show_sessions() {
151
+ local data=$(get_recent_sessions)
152
+ if [ -z "$data" ]; then
153
+ echo " No sessions found."
154
+ return
155
+ fi
156
+
157
+ local current_num=""
158
+
159
+ echo "$data" | while IFS='|' read -r key value; do
160
+ case "$key" in
161
+ SESSION)
162
+ current_num="$value"
163
+ echo ""
164
+ echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
165
+ echo " [$value]"
166
+ ;;
167
+ ID)
168
+ echo " ID: $value"
169
+ ;;
170
+ MESSAGES)
171
+ printf " Messages: %s" "$value"
172
+ ;;
173
+ SIZE)
174
+ printf " | Size: %s\n" "$value"
175
+ ;;
176
+ LAST_ACTIVE)
177
+ echo " Active: $value"
178
+ ;;
179
+ STARTED)
180
+ echo " Started: $value"
181
+ ;;
182
+ LAST_SEEN)
183
+ echo " Last: $value"
184
+ ;;
185
+ FIRST_PROMPT)
186
+ if [ -n "$value" ]; then
187
+ echo " First: \"$value\""
188
+ fi
189
+ ;;
190
+ LAST_PROMPT)
191
+ if [ -n "$value" ]; then
192
+ echo " Latest: \"$value\""
193
+ fi
194
+ ;;
195
+ esac
196
+ done
197
+ echo ""
198
+ }
199
+
200
+ # Count running Claude instances
201
+ count_claude_instances() {
202
+ pgrep -x "claude" 2>/dev/null | wc -l
203
+ }
204
+
205
+ # Save session state
206
+ save_session_state() {
207
+ local session_id="$1"
208
+ local flags="${2:---dangerously-skip-permissions}"
209
+ mkdir -p "${SESSIONS_DIR}"
210
+ cat > "${STATE_FILE}" << EOF
211
+ {
212
+ "sessionId": "${session_id}",
213
+ "flags": "${flags}",
214
+ "terminalId": "${TERMINAL_ID}",
215
+ "timestamp": $(date +%s)
216
+ }
217
+ EOF
218
+ }
219
+
220
+ # Get last session for this terminal
221
+ get_terminal_last_session() {
222
+ if [ -f "${STATE_FILE}" ]; then
223
+ node -e "try{console.log(require('${STATE_FILE}').sessionId||'')}catch(e){}" 2>/dev/null
224
+ fi
225
+ }
226
+
227
+ # Interactive session picker
228
+ claude_prompt() {
229
+ # Only in interactive shells
230
+ [[ $- != *i* ]] && return 0
231
+
232
+ # Check if claude exists
233
+ if ! command -v claude &>/dev/null; then
234
+ return 0
235
+ fi
236
+
237
+ local running=$(count_claude_instances)
238
+ local last_session=$(get_terminal_last_session)
239
+
240
+ echo ""
241
+ echo "╭─────────────────────────────────────────────────────────╮"
242
+ echo "│ Claude Session Manager │"
243
+ echo "╰─────────────────────────────────────────────────────────╯"
244
+
245
+ if [ "$running" -gt 0 ]; then
246
+ echo " ($running Claude instance(s) running in other terminals)"
247
+ fi
248
+ echo ""
249
+
250
+ # Show options
251
+ echo " [c] Continue last session for this terminal"
252
+ if [ -n "$last_session" ]; then
253
+ echo " └─ ${last_session:0:8}..."
254
+ fi
255
+ echo " [r] Resume a specific session (pick from list)"
256
+ echo " [n] Start new session"
257
+ echo " [s] Skip - just give me a shell"
258
+ echo ""
259
+
260
+ # Read choice with timeout
261
+ local choice
262
+ read -t 30 -n 1 -p " Choice [c/r/n/s]: " choice
263
+ echo ""
264
+
265
+ case "$choice" in
266
+ c|C|"")
267
+ # Continue last session (default on Enter or timeout)
268
+ if [ -n "$last_session" ]; then
269
+ echo ""
270
+ echo " Resuming session ${last_session:0:8}..."
271
+ claude -r "$last_session" --dangerously-skip-permissions
272
+ save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)"
273
+ else
274
+ echo ""
275
+ echo " No previous session for this terminal, starting new..."
276
+ claude --dangerously-skip-permissions
277
+ save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)"
278
+ fi
279
+ ;;
280
+ r|R)
281
+ # Show session list with full details
282
+ echo ""
283
+ echo " Recent Sessions"
284
+ show_sessions
285
+
286
+ # Get session IDs for selection
287
+ local session_ids=$(get_recent_sessions | grep "^ID|" | cut -d'|' -f2)
288
+
289
+ read -p " Enter number (or 'q' to cancel): " session_num
290
+
291
+ if [ "$session_num" = "q" ] || [ -z "$session_num" ]; then
292
+ echo " Cancelled."
293
+ return 0
294
+ fi
295
+
296
+ local selected_id=$(echo "$session_ids" | sed -n "${session_num}p")
297
+ if [ -n "$selected_id" ]; then
298
+ echo ""
299
+ echo " Resuming session: $selected_id"
300
+ claude -r "$selected_id" --dangerously-skip-permissions
301
+ save_session_state "$selected_id"
302
+ else
303
+ echo " Invalid selection."
304
+ fi
305
+ ;;
306
+ n|N)
307
+ # Start new session
308
+ echo ""
309
+ echo " Starting new Claude session..."
310
+ claude --dangerously-skip-permissions
311
+ save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)"
312
+ ;;
313
+ s|S)
314
+ # Skip - just shell
315
+ echo ""
316
+ echo " Okay, just a shell. Type 'claude' or 'cr' when you want Claude."
317
+ ;;
318
+ *)
319
+ echo ""
320
+ echo " Unknown option. Type 'claude' to start manually."
321
+ ;;
322
+ esac
323
+ }
324
+
325
+ # Aliases for manual use
326
+ alias cr='claude -c --dangerously-skip-permissions'
327
+ alias claude-resume='claude -c --dangerously-skip-permissions'
328
+ alias claude-pick='claude -r --dangerously-skip-permissions'
329
+ alias claude-new='claude --dangerously-skip-permissions'
330
+
331
+ # Export for manual use
332
+ export -f get_recent_sessions
333
+ export -f save_session_state
334
+ export TERMINAL_ID
335
+
336
+ # Show the interactive prompt by default.
337
+ # Press 's' to skip and just get a shell.
338
+ # Set CLAUDE_NO_PROMPT=true to disable entirely.
339
+
340
+ alias claude-menu='claude_prompt'
341
+
342
+ if [ "${CLAUDE_NO_PROMPT}" != "true" ]; then
343
+ claude_prompt
344
+ fi
@@ -0,0 +1,171 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # Claude Code Setup Script for Replit
4
+ # =============================================================================
5
+ # This script ensures Claude Code is properly set up after container restarts.
6
+ # It handles:
7
+ # 1. Symlink for conversation history persistence (~/.claude)
8
+ # 2. Symlink for Claude binary (~/.local/bin/claude)
9
+ # 3. Authentication persistence (credentials stored in workspace)
10
+ # 4. Auto-installation if Claude is missing
11
+ #
12
+ # Run this script on every container restart via .config/bashrc or .replit
13
+ # =============================================================================
14
+
15
+ set -e
16
+
17
+ # Configuration
18
+ WORKSPACE="/home/runner/workspace"
19
+ CLAUDE_PERSISTENT="${WORKSPACE}/.claude-persistent"
20
+ CLAUDE_LOCAL_SHARE="${WORKSPACE}/.local/share/claude"
21
+ CLAUDE_VERSIONS="${CLAUDE_LOCAL_SHARE}/versions"
22
+
23
+ # Target locations (ephemeral, need symlinks)
24
+ CLAUDE_SYMLINK="${HOME}/.claude"
25
+ LOCAL_BIN="${HOME}/.local/bin"
26
+ LOCAL_SHARE_CLAUDE="${HOME}/.local/share/claude"
27
+
28
+ # Logging helper
29
+ log() {
30
+ if [[ $- == *i* ]]; then
31
+ echo "$1"
32
+ fi
33
+ }
34
+
35
+ # =============================================================================
36
+ # Step 1: Ensure persistent directories exist
37
+ # =============================================================================
38
+ mkdir -p "${CLAUDE_PERSISTENT}"
39
+ mkdir -p "${CLAUDE_VERSIONS}"
40
+ mkdir -p "${LOCAL_BIN}"
41
+ mkdir -p "${HOME}/.local/share"
42
+
43
+ # =============================================================================
44
+ # Step 2: Create ~/.claude symlink for conversation history & credentials
45
+ # =============================================================================
46
+ if [ ! -L "${CLAUDE_SYMLINK}" ] || [ "$(readlink -f "${CLAUDE_SYMLINK}")" != "${CLAUDE_PERSISTENT}" ]; then
47
+ rm -rf "${CLAUDE_SYMLINK}" 2>/dev/null || true
48
+ ln -sf "${CLAUDE_PERSISTENT}" "${CLAUDE_SYMLINK}"
49
+ log "✅ Claude history symlink: ~/.claude -> ${CLAUDE_PERSISTENT}"
50
+ fi
51
+
52
+ # =============================================================================
53
+ # Step 3: Create ~/.local/share/claude symlink for installed versions
54
+ # =============================================================================
55
+ if [ ! -L "${LOCAL_SHARE_CLAUDE}" ] || [ "$(readlink -f "${LOCAL_SHARE_CLAUDE}")" != "${CLAUDE_LOCAL_SHARE}" ]; then
56
+ rm -rf "${LOCAL_SHARE_CLAUDE}" 2>/dev/null || true
57
+ ln -sf "${CLAUDE_LOCAL_SHARE}" "${LOCAL_SHARE_CLAUDE}"
58
+ log "✅ Claude versions symlink: ~/.local/share/claude -> ${CLAUDE_LOCAL_SHARE}"
59
+ fi
60
+
61
+ # =============================================================================
62
+ # Step 4: Find latest Claude version and create binary symlink
63
+ # =============================================================================
64
+ LATEST_VERSION=""
65
+ if [ -d "${CLAUDE_VERSIONS}" ]; then
66
+ LATEST_VERSION=$(ls -1 "${CLAUDE_VERSIONS}" 2>/dev/null | sort -V | tail -n1)
67
+ fi
68
+
69
+ if [ -n "${LATEST_VERSION}" ] && [ -f "${CLAUDE_VERSIONS}/${LATEST_VERSION}" ]; then
70
+ CLAUDE_BINARY="${CLAUDE_VERSIONS}/${LATEST_VERSION}"
71
+
72
+ # Create or update the binary symlink
73
+ if [ ! -L "${LOCAL_BIN}/claude" ] || [ "$(readlink -f "${LOCAL_BIN}/claude")" != "${CLAUDE_BINARY}" ]; then
74
+ rm -f "${LOCAL_BIN}/claude" 2>/dev/null || true
75
+ ln -sf "${CLAUDE_BINARY}" "${LOCAL_BIN}/claude"
76
+ log "✅ Claude binary symlink: ~/.local/bin/claude -> ${CLAUDE_BINARY}"
77
+ fi
78
+ else
79
+ # Claude not installed - install it
80
+ log "⚠️ Claude Code not found, installing..."
81
+
82
+ # Install Claude Code using npm
83
+ if command -v npm &> /dev/null; then
84
+ npm install -g @anthropic-ai/claude-code 2>/dev/null || {
85
+ log "❌ Failed to install Claude Code via npm"
86
+ log " Try running: npm install -g @anthropic-ai/claude-code"
87
+ }
88
+
89
+ # After npm install, the binary should be available
90
+ # Move it to our persistent location
91
+ if command -v claude &> /dev/null; then
92
+ INSTALLED_PATH=$(which claude)
93
+ if [ -f "${INSTALLED_PATH}" ] && [ ! -L "${INSTALLED_PATH}" ]; then
94
+ # Get version
95
+ VERSION=$(claude --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1)
96
+ if [ -n "${VERSION}" ]; then
97
+ cp "${INSTALLED_PATH}" "${CLAUDE_VERSIONS}/${VERSION}"
98
+ chmod +x "${CLAUDE_VERSIONS}/${VERSION}"
99
+ rm -f "${LOCAL_BIN}/claude" 2>/dev/null || true
100
+ ln -sf "${CLAUDE_VERSIONS}/${VERSION}" "${LOCAL_BIN}/claude"
101
+ log "✅ Claude Code ${VERSION} installed and persisted"
102
+ fi
103
+ fi
104
+ fi
105
+ else
106
+ log "❌ npm not found, cannot install Claude Code"
107
+ fi
108
+ fi
109
+
110
+ # =============================================================================
111
+ # Step 5: Ensure PATH includes ~/.local/bin
112
+ # =============================================================================
113
+ if [[ ":$PATH:" != *":${LOCAL_BIN}:"* ]]; then
114
+ export PATH="${LOCAL_BIN}:$PATH"
115
+ fi
116
+
117
+ # =============================================================================
118
+ # Step 6: Verify authentication
119
+ # =============================================================================
120
+ CREDENTIALS_FILE="${CLAUDE_PERSISTENT}/.credentials.json"
121
+ if [ -f "${CREDENTIALS_FILE}" ]; then
122
+ # Check if credentials are valid (not expired)
123
+ if command -v node &> /dev/null; then
124
+ AUTH_INFO=$(node -e "
125
+ try {
126
+ const creds = require('${CREDENTIALS_FILE}');
127
+ const oauth = creds.claudeAiOauth;
128
+ const apiKey = creds.primaryApiKey;
129
+ if (apiKey) {
130
+ // Long-lived API key - doesn't expire
131
+ console.log('apikey');
132
+ } else if (oauth && oauth.expiresAt) {
133
+ console.log(oauth.expiresAt);
134
+ }
135
+ } catch(e) {}
136
+ " 2>/dev/null)
137
+
138
+ if [ "${AUTH_INFO}" = "apikey" ]; then
139
+ log "✅ Claude authentication: long-lived token (no expiration)"
140
+ elif [ -n "${AUTH_INFO}" ]; then
141
+ CURRENT_TIME=$(node -e "console.log(Date.now())" 2>/dev/null)
142
+ if [ -n "${CURRENT_TIME}" ] && [ "${AUTH_INFO}" -gt "${CURRENT_TIME}" ]; then
143
+ # Calculate time remaining
144
+ HOURS_LEFT=$(node -e "console.log(Math.floor((${AUTH_INFO} - ${CURRENT_TIME}) / 1000 / 60 / 60))" 2>/dev/null)
145
+ if [ "${HOURS_LEFT}" -lt 2 ]; then
146
+ log "⚠️ Claude authentication: expires in ${HOURS_LEFT}h - run 'claude login' soon"
147
+ else
148
+ log "✅ Claude authentication: valid (${HOURS_LEFT}h remaining)"
149
+ fi
150
+ else
151
+ log "⚠️ Claude authentication: expired, run 'claude login' to re-authenticate"
152
+ log " 💡 Tip: Run 'claude setup-token' for a long-lived token that won't expire"
153
+ fi
154
+ fi
155
+ else
156
+ log "✅ Claude credentials file exists (persisted in workspace)"
157
+ fi
158
+ else
159
+ log "⚠️ No Claude credentials found. Run 'claude login' to authenticate"
160
+ log " 💡 Tip: Run 'claude setup-token' for a long-lived token that won't expire"
161
+ fi
162
+
163
+ # =============================================================================
164
+ # Summary (only in interactive shells)
165
+ # =============================================================================
166
+ if [[ $- == *i* ]] && [ -n "${LATEST_VERSION}" ]; then
167
+ if command -v claude &> /dev/null; then
168
+ CLAUDE_VERSION=$(claude --version 2>/dev/null | head -1)
169
+ log "✅ Claude Code ready: ${CLAUDE_VERSION}"
170
+ fi
171
+ fi