replit-tools 1.0.5 → 1.0.7
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 +54 -20
- package/index.js +72 -27
- package/package.json +1 -1
- package/scripts/claude-auth-refresh.sh +221 -0
- package/scripts/setup-claude-code.sh +74 -47
package/README.md
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
# DATA Tools
|
|
2
2
|
|
|
3
|
-
**One command to set up Claude Code and Codex CLI on Replit with full persistence.**
|
|
3
|
+
**One command to set up Claude Code and Codex CLI on Replit with full persistence and automatic token refresh.**
|
|
4
4
|
|
|
5
5
|
When Replit containers restart, everything outside `/home/runner/workspace/` is wiped - including installed CLIs, conversations, auth tokens, and command history. DATA Tools fixes all of that.
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npx replit-tools
|
|
10
|
+
npx -y replit-tools
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
(The `-y` skips the "Ok to proceed?" prompt)
|
|
14
|
+
|
|
13
15
|
That's it. The installer will:
|
|
14
16
|
|
|
15
17
|
1. **Install Claude Code** (if not already installed)
|
|
16
18
|
2. **Install OpenAI Codex CLI** (if not already installed)
|
|
17
19
|
3. **Detect existing config** and preserve your data
|
|
18
20
|
4. **Set up persistence** so everything survives restarts
|
|
19
|
-
5. **
|
|
21
|
+
5. **Auto-refresh OAuth tokens** before they expire
|
|
22
|
+
6. **Launch the session picker** so you can start working immediately
|
|
20
23
|
|
|
21
24
|
## What Gets Installed
|
|
22
25
|
|
|
@@ -38,15 +41,36 @@ Both are installed only if not already present. Existing installations are prese
|
|
|
38
41
|
| Bash history | `.persistent-home/` | Yes |
|
|
39
42
|
| Per-terminal sessions | `.claude-sessions/` | Yes |
|
|
40
43
|
|
|
44
|
+
## Automatic Token Refresh
|
|
45
|
+
|
|
46
|
+
Claude OAuth tokens expire every **8-12 hours**. DATA Tools automatically refreshes them:
|
|
47
|
+
|
|
48
|
+
- **On every shell start**: Checks token expiry and refreshes if < 2 hours remaining
|
|
49
|
+
- **When expired**: Attempts automatic refresh using the stored refresh token
|
|
50
|
+
- **Transparent**: You'll see `🔄 Token expires in 1h, refreshing...` then `✅ Token refreshed (11h remaining)`
|
|
51
|
+
|
|
52
|
+
This means you can leave overnight and come back to a working session - no more `claude login` every morning.
|
|
53
|
+
|
|
54
|
+
### Manual Token Commands
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Check token status
|
|
58
|
+
/home/runner/workspace/scripts/claude-auth-refresh.sh --status
|
|
59
|
+
|
|
60
|
+
# Force refresh now
|
|
61
|
+
/home/runner/workspace/scripts/claude-auth-refresh.sh --force
|
|
62
|
+
|
|
63
|
+
# Or use a permanent API token (never expires)
|
|
64
|
+
claude setup-token
|
|
65
|
+
```
|
|
66
|
+
|
|
41
67
|
## The Session Picker
|
|
42
68
|
|
|
43
69
|
After installation (and on every new shell), you'll see:
|
|
44
70
|
|
|
45
71
|
```
|
|
46
|
-
✅ Claude
|
|
47
|
-
✅
|
|
48
|
-
|
|
49
|
-
✅ Claude authentication: valid (23h remaining)
|
|
72
|
+
✅ Claude authentication: valid (11h remaining)
|
|
73
|
+
✅ Claude Code ready: 2.0.71 (Claude Code)
|
|
50
74
|
|
|
51
75
|
╭─────────────────────────────────────────────────────────╮
|
|
52
76
|
│ Claude Session Manager │
|
|
@@ -101,10 +125,10 @@ Press `c` to continue YOUR terminal's last session. Other terminals are unaffect
|
|
|
101
125
|
The installer creates symlinks from ephemeral locations to persistent workspace storage:
|
|
102
126
|
|
|
103
127
|
```
|
|
104
|
-
~/.claude
|
|
105
|
-
~/.codex
|
|
128
|
+
~/.claude → /workspace/.claude-persistent/
|
|
129
|
+
~/.codex → /workspace/.codex-persistent/
|
|
106
130
|
~/.local/share/claude → /workspace/.local/share/claude/
|
|
107
|
-
~/.local/bin/claude
|
|
131
|
+
~/.local/bin/claude → /workspace/.local/share/claude/versions/X.X.X
|
|
108
132
|
```
|
|
109
133
|
|
|
110
134
|
Three layers ensure setup runs on every restart:
|
|
@@ -117,7 +141,8 @@ Three layers ensure setup runs on every restart:
|
|
|
117
141
|
The installer checks for:
|
|
118
142
|
|
|
119
143
|
- **`CLAUDE_CONFIG_DIR`** - Respects custom Claude config directory if set in Replit Secrets
|
|
120
|
-
-
|
|
144
|
+
- **`CODEX_HOME`** - Respects custom Codex config directory
|
|
145
|
+
- **Existing persistent config** - Uses your existing config if present
|
|
121
146
|
- **Replit Secrets** - Detects `ANTHROPIC_API_KEY` and `OPENAI_API_KEY`
|
|
122
147
|
- **Existing installations** - Won't reinstall Claude or Codex if already present
|
|
123
148
|
- **Existing data in ~/.claude** - Moves it to persistent storage instead of overwriting
|
|
@@ -150,7 +175,7 @@ If you set these in your Replit Secrets to paths inside `/home/runner/workspace/
|
|
|
150
175
|
### Option 1: npx (recommended)
|
|
151
176
|
|
|
152
177
|
```bash
|
|
153
|
-
npx replit-tools
|
|
178
|
+
npx -y replit-tools
|
|
154
179
|
```
|
|
155
180
|
|
|
156
181
|
### Option 2: curl
|
|
@@ -187,13 +212,13 @@ export CLAUDE_NO_PROMPT=true
|
|
|
187
212
|
|
|
188
213
|
Add to `.config/bashrc` to make permanent.
|
|
189
214
|
|
|
190
|
-
###
|
|
215
|
+
### Use a permanent API token
|
|
191
216
|
|
|
192
217
|
```bash
|
|
193
218
|
claude setup-token
|
|
194
219
|
```
|
|
195
220
|
|
|
196
|
-
Creates a long-lived API token that
|
|
221
|
+
Creates a long-lived API token that never expires (recommended for unattended use).
|
|
197
222
|
|
|
198
223
|
## Files Created
|
|
199
224
|
|
|
@@ -205,9 +230,11 @@ workspace/
|
|
|
205
230
|
├── .local/share/claude/ # Claude binary versions
|
|
206
231
|
├── .persistent-home/ # Bash history
|
|
207
232
|
├── .config/bashrc # Shell startup config
|
|
233
|
+
├── logs/ # Auth refresh logs
|
|
208
234
|
├── scripts/
|
|
209
|
-
│ ├── setup-claude-code.sh
|
|
210
|
-
│
|
|
235
|
+
│ ├── setup-claude-code.sh # Main setup script
|
|
236
|
+
│ ├── claude-session-manager.sh # Interactive session picker
|
|
237
|
+
│ └── claude-auth-refresh.sh # OAuth token auto-refresh
|
|
211
238
|
└── .gitignore # Updated to ignore credential dirs
|
|
212
239
|
```
|
|
213
240
|
|
|
@@ -225,18 +252,25 @@ source /home/runner/workspace/scripts/setup-claude-code.sh
|
|
|
225
252
|
source /home/runner/workspace/.config/bashrc
|
|
226
253
|
```
|
|
227
254
|
|
|
228
|
-
### Auth
|
|
255
|
+
### Auth keeps expiring
|
|
256
|
+
|
|
257
|
+
The auto-refresh should handle this, but if it fails:
|
|
229
258
|
|
|
230
259
|
```bash
|
|
231
|
-
|
|
232
|
-
|
|
260
|
+
# Check why refresh failed
|
|
261
|
+
cat /home/runner/workspace/logs/auth-refresh.log
|
|
262
|
+
|
|
263
|
+
# Manual refresh
|
|
264
|
+
/home/runner/workspace/scripts/claude-auth-refresh.sh --force
|
|
265
|
+
|
|
266
|
+
# Or use permanent token (recommended)
|
|
233
267
|
claude setup-token
|
|
234
268
|
```
|
|
235
269
|
|
|
236
270
|
### Symlinks broken
|
|
237
271
|
|
|
238
272
|
```bash
|
|
239
|
-
npx replit-tools
|
|
273
|
+
npx -y replit-tools
|
|
240
274
|
```
|
|
241
275
|
|
|
242
276
|
Running the installer again is safe - it preserves existing data.
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { execSync, spawn } = require('child_process');
|
|
3
|
+
const { execSync, spawn, spawnSync } = require('child_process');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
@@ -8,6 +8,45 @@ const os = require('os');
|
|
|
8
8
|
const WORKSPACE = '/home/runner/workspace';
|
|
9
9
|
const HOME = os.homedir();
|
|
10
10
|
|
|
11
|
+
// Helper to run commands safely without crashing the installer
|
|
12
|
+
function safeExec(cmd, options = {}) {
|
|
13
|
+
try {
|
|
14
|
+
const result = spawnSync('bash', ['-c', cmd], {
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
timeout: options.timeout || 120000,
|
|
17
|
+
env: options.env || process.env,
|
|
18
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
19
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (options.showOutput && result.stdout) {
|
|
23
|
+
// Show condensed output
|
|
24
|
+
const lines = result.stdout.trim().split('\n');
|
|
25
|
+
if (lines.length <= 5) {
|
|
26
|
+
lines.forEach(l => console.log(` ${l}`));
|
|
27
|
+
} else {
|
|
28
|
+
console.log(` ${lines[0]}`);
|
|
29
|
+
console.log(` ... (${lines.length - 2} more lines)`);
|
|
30
|
+
console.log(` ${lines[lines.length - 1]}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
success: result.status === 0,
|
|
36
|
+
stdout: result.stdout || '',
|
|
37
|
+
stderr: result.stderr || '',
|
|
38
|
+
status: result.status
|
|
39
|
+
};
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
stdout: '',
|
|
44
|
+
stderr: err.message,
|
|
45
|
+
status: -1
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
11
50
|
// Wrap everything in try-catch to prevent crashes
|
|
12
51
|
try {
|
|
13
52
|
main();
|
|
@@ -205,21 +244,24 @@ function main() {
|
|
|
205
244
|
|
|
206
245
|
if (!claudeInstalled) {
|
|
207
246
|
console.log('📦 Installing Claude Code...');
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
247
|
+
const installEnv = {
|
|
248
|
+
...process.env,
|
|
249
|
+
CLAUDE_CONFIG_DIR: claudePersistentDir,
|
|
250
|
+
CLAUDE_WORKSPACE_DIR: claudePersistentDir
|
|
251
|
+
};
|
|
252
|
+
const result = safeExec('curl -fsSL https://claude.ai/install.sh | bash', {
|
|
253
|
+
env: installEnv,
|
|
254
|
+
timeout: 180000, // 3 minute timeout
|
|
255
|
+
showOutput: true
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (result.success) {
|
|
220
259
|
console.log('✅ Claude Code installed');
|
|
221
|
-
}
|
|
260
|
+
} else {
|
|
222
261
|
console.log('⚠️ Claude Code installation had issues (may still work)');
|
|
262
|
+
if (result.stderr && result.stderr.length < 200) {
|
|
263
|
+
console.log(` ${result.stderr.trim()}`);
|
|
264
|
+
}
|
|
223
265
|
}
|
|
224
266
|
} else {
|
|
225
267
|
const version = claudeVersions.sort().pop() || 'installed';
|
|
@@ -234,20 +276,23 @@ function main() {
|
|
|
234
276
|
|
|
235
277
|
if (!codexInstalled) {
|
|
236
278
|
console.log('📦 Installing OpenAI Codex CLI...');
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
279
|
+
const installEnv = {
|
|
280
|
+
...process.env,
|
|
281
|
+
CODEX_HOME: codexPersistentDir
|
|
282
|
+
};
|
|
283
|
+
const result = safeExec('npm i -g @openai/codex', {
|
|
284
|
+
env: installEnv,
|
|
285
|
+
timeout: 180000, // 3 minute timeout
|
|
286
|
+
showOutput: true
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (result.success) {
|
|
248
290
|
console.log('✅ Codex CLI installed');
|
|
249
|
-
}
|
|
291
|
+
} else {
|
|
250
292
|
console.log('⚠️ Codex installation had issues (may still work)');
|
|
293
|
+
if (result.stderr && result.stderr.length < 200) {
|
|
294
|
+
console.log(` ${result.stderr.trim()}`);
|
|
295
|
+
}
|
|
251
296
|
}
|
|
252
297
|
} else {
|
|
253
298
|
console.log('✅ Codex CLI already installed');
|
|
@@ -375,7 +420,7 @@ function main() {
|
|
|
375
420
|
console.log('');
|
|
376
421
|
console.log('📝 Installing scripts...');
|
|
377
422
|
|
|
378
|
-
const scripts = ['setup-claude-code.sh', 'claude-session-manager.sh'];
|
|
423
|
+
const scripts = ['setup-claude-code.sh', 'claude-session-manager.sh', 'claude-auth-refresh.sh'];
|
|
379
424
|
scripts.forEach(script => {
|
|
380
425
|
const srcPath = path.join(scriptsDir, script);
|
|
381
426
|
const destPath = path.join(targetScriptsDir, script);
|
package/package.json
CHANGED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Claude OAuth Token Auto-Refresh
|
|
3
|
+
# Automatically refreshes Claude Code OAuth tokens before expiration
|
|
4
|
+
# Part of DATA Tools - https://github.com/stevemoraco/DATAtools
|
|
5
|
+
|
|
6
|
+
CREDENTIALS_FILE="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json"
|
|
7
|
+
OAUTH_ENDPOINT="https://console.anthropic.com/v1/oauth/token"
|
|
8
|
+
CLIENT_ID="9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
|
9
|
+
REFRESH_THRESHOLD_HOURS=2 # Refresh when less than 2 hours remaining
|
|
10
|
+
LOG_FILE="/home/runner/workspace/logs/auth-refresh.log"
|
|
11
|
+
|
|
12
|
+
# Logging function
|
|
13
|
+
log() {
|
|
14
|
+
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
15
|
+
echo "[$timestamp] $1" >> "$LOG_FILE" 2>/dev/null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Check if jq is available, if not use node for JSON parsing
|
|
19
|
+
parse_json() {
|
|
20
|
+
local json="$1"
|
|
21
|
+
local key="$2"
|
|
22
|
+
|
|
23
|
+
if command -v jq &>/dev/null; then
|
|
24
|
+
echo "$json" | jq -r ".$key" 2>/dev/null
|
|
25
|
+
else
|
|
26
|
+
# Fallback to node
|
|
27
|
+
echo "$json" | node -e "
|
|
28
|
+
let data = '';
|
|
29
|
+
process.stdin.on('data', chunk => data += chunk);
|
|
30
|
+
process.stdin.on('end', () => {
|
|
31
|
+
try {
|
|
32
|
+
const obj = JSON.parse(data);
|
|
33
|
+
const keys = '$key'.split('.');
|
|
34
|
+
let val = obj;
|
|
35
|
+
for (const k of keys) val = val[k];
|
|
36
|
+
console.log(val);
|
|
37
|
+
} catch(e) { console.log(''); }
|
|
38
|
+
});
|
|
39
|
+
" 2>/dev/null
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Get current timestamp in milliseconds
|
|
44
|
+
get_current_time_ms() {
|
|
45
|
+
echo $(($(date +%s) * 1000))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Check token status and return remaining time in hours
|
|
49
|
+
check_token_status() {
|
|
50
|
+
if [ ! -f "$CREDENTIALS_FILE" ]; then
|
|
51
|
+
echo "no_file"
|
|
52
|
+
return 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
local creds=$(cat "$CREDENTIALS_FILE" 2>/dev/null)
|
|
56
|
+
if [ -z "$creds" ]; then
|
|
57
|
+
echo "empty_file"
|
|
58
|
+
return 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
local expires_at=$(parse_json "$creds" "claudeAiOauth.expiresAt")
|
|
62
|
+
if [ -z "$expires_at" ] || [ "$expires_at" = "null" ]; then
|
|
63
|
+
echo "no_expiry"
|
|
64
|
+
return 1
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
local current_time=$(get_current_time_ms)
|
|
68
|
+
local remaining_ms=$((expires_at - current_time))
|
|
69
|
+
local remaining_hours=$((remaining_ms / 1000 / 60 / 60))
|
|
70
|
+
|
|
71
|
+
if [ $remaining_ms -le 0 ]; then
|
|
72
|
+
echo "expired"
|
|
73
|
+
return 1
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
echo "$remaining_hours"
|
|
77
|
+
return 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Refresh the OAuth token
|
|
81
|
+
refresh_token() {
|
|
82
|
+
local creds=$(cat "$CREDENTIALS_FILE" 2>/dev/null)
|
|
83
|
+
local refresh_token=$(parse_json "$creds" "claudeAiOauth.refreshToken")
|
|
84
|
+
|
|
85
|
+
if [ -z "$refresh_token" ] || [ "$refresh_token" = "null" ]; then
|
|
86
|
+
log "ERROR: No refresh token found"
|
|
87
|
+
return 1
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
log "Attempting token refresh..."
|
|
91
|
+
|
|
92
|
+
# Make the refresh request
|
|
93
|
+
local response=$(curl -s -X POST "$OAUTH_ENDPOINT" \
|
|
94
|
+
-H "Content-Type: application/json" \
|
|
95
|
+
-d "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"$refresh_token\",\"client_id\":\"$CLIENT_ID\"}" \
|
|
96
|
+
2>/dev/null)
|
|
97
|
+
|
|
98
|
+
if [ -z "$response" ]; then
|
|
99
|
+
log "ERROR: No response from OAuth endpoint"
|
|
100
|
+
return 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Check for error in response
|
|
104
|
+
local error=$(parse_json "$response" "error")
|
|
105
|
+
if [ -n "$error" ] && [ "$error" != "null" ]; then
|
|
106
|
+
log "ERROR: OAuth refresh failed: $error"
|
|
107
|
+
return 1
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# Extract new tokens
|
|
111
|
+
local new_access_token=$(parse_json "$response" "access_token")
|
|
112
|
+
local new_refresh_token=$(parse_json "$response" "refresh_token")
|
|
113
|
+
local expires_in=$(parse_json "$response" "expires_in")
|
|
114
|
+
|
|
115
|
+
if [ -z "$new_access_token" ] || [ "$new_access_token" = "null" ]; then
|
|
116
|
+
log "ERROR: No access token in response"
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Calculate new expiry time (expires_in is in seconds)
|
|
121
|
+
local current_time=$(get_current_time_ms)
|
|
122
|
+
local new_expires_at=$((current_time + expires_in * 1000))
|
|
123
|
+
|
|
124
|
+
# Use the new refresh token if provided, otherwise keep the old one
|
|
125
|
+
if [ -z "$new_refresh_token" ] || [ "$new_refresh_token" = "null" ]; then
|
|
126
|
+
new_refresh_token="$refresh_token"
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# Backup the old credentials
|
|
130
|
+
cp "$CREDENTIALS_FILE" "${CREDENTIALS_FILE}.backup" 2>/dev/null
|
|
131
|
+
|
|
132
|
+
# Update credentials file using node (more reliable for JSON manipulation)
|
|
133
|
+
node -e "
|
|
134
|
+
const fs = require('fs');
|
|
135
|
+
const creds = JSON.parse(fs.readFileSync('$CREDENTIALS_FILE', 'utf8'));
|
|
136
|
+
creds.claudeAiOauth.accessToken = '$new_access_token';
|
|
137
|
+
creds.claudeAiOauth.refreshToken = '$new_refresh_token';
|
|
138
|
+
creds.claudeAiOauth.expiresAt = $new_expires_at;
|
|
139
|
+
fs.writeFileSync('$CREDENTIALS_FILE', JSON.stringify(creds));
|
|
140
|
+
" 2>/dev/null
|
|
141
|
+
|
|
142
|
+
if [ $? -eq 0 ]; then
|
|
143
|
+
log "SUCCESS: Token refreshed, expires in $((expires_in / 3600)) hours"
|
|
144
|
+
return 0
|
|
145
|
+
else
|
|
146
|
+
# Restore backup on failure
|
|
147
|
+
mv "${CREDENTIALS_FILE}.backup" "$CREDENTIALS_FILE" 2>/dev/null
|
|
148
|
+
log "ERROR: Failed to update credentials file"
|
|
149
|
+
return 1
|
|
150
|
+
fi
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Main function - called from setup script
|
|
154
|
+
auto_refresh_if_needed() {
|
|
155
|
+
local status=$(check_token_status)
|
|
156
|
+
|
|
157
|
+
case "$status" in
|
|
158
|
+
"no_file"|"empty_file"|"no_expiry")
|
|
159
|
+
# No credentials, user needs to login
|
|
160
|
+
return 1
|
|
161
|
+
;;
|
|
162
|
+
"expired")
|
|
163
|
+
echo "⚠️ Token expired, attempting refresh..."
|
|
164
|
+
if refresh_token; then
|
|
165
|
+
echo "✅ Token refreshed successfully"
|
|
166
|
+
return 0
|
|
167
|
+
else
|
|
168
|
+
echo "❌ Token refresh failed - run: claude login"
|
|
169
|
+
return 1
|
|
170
|
+
fi
|
|
171
|
+
;;
|
|
172
|
+
*)
|
|
173
|
+
# status is remaining hours
|
|
174
|
+
local remaining=$status
|
|
175
|
+
if [ "$remaining" -lt "$REFRESH_THRESHOLD_HOURS" ]; then
|
|
176
|
+
echo "🔄 Token expires in ${remaining}h, refreshing..."
|
|
177
|
+
if refresh_token; then
|
|
178
|
+
local new_status=$(check_token_status)
|
|
179
|
+
echo "✅ Token refreshed (${new_status}h remaining)"
|
|
180
|
+
return 0
|
|
181
|
+
else
|
|
182
|
+
echo "⚠️ Refresh failed, ${remaining}h remaining"
|
|
183
|
+
return 0 # Don't fail, token still works
|
|
184
|
+
fi
|
|
185
|
+
else
|
|
186
|
+
# Token is fine, no refresh needed
|
|
187
|
+
return 0
|
|
188
|
+
fi
|
|
189
|
+
;;
|
|
190
|
+
esac
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Command line interface
|
|
194
|
+
case "${1:-}" in
|
|
195
|
+
--status)
|
|
196
|
+
status=$(check_token_status)
|
|
197
|
+
case "$status" in
|
|
198
|
+
"no_file") echo "❌ No credentials file found" ;;
|
|
199
|
+
"empty_file") echo "❌ Credentials file is empty" ;;
|
|
200
|
+
"no_expiry") echo "❌ No expiry timestamp found" ;;
|
|
201
|
+
"expired") echo "❌ Token has expired" ;;
|
|
202
|
+
*) echo "✅ Token valid (${status}h remaining)" ;;
|
|
203
|
+
esac
|
|
204
|
+
;;
|
|
205
|
+
--force)
|
|
206
|
+
echo "Forcing token refresh..."
|
|
207
|
+
if refresh_token; then
|
|
208
|
+
echo "✅ Token refreshed"
|
|
209
|
+
else
|
|
210
|
+
echo "❌ Refresh failed"
|
|
211
|
+
exit 1
|
|
212
|
+
fi
|
|
213
|
+
;;
|
|
214
|
+
--auto)
|
|
215
|
+
auto_refresh_if_needed
|
|
216
|
+
;;
|
|
217
|
+
*)
|
|
218
|
+
# Default: auto refresh if needed (silent unless action taken)
|
|
219
|
+
auto_refresh_if_needed
|
|
220
|
+
;;
|
|
221
|
+
esac
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
# 2. Symlink for Claude binary (~/.local/bin/claude)
|
|
9
9
|
# 3. Authentication persistence (credentials stored in workspace)
|
|
10
10
|
# 4. Auto-installation if Claude is missing
|
|
11
|
+
# 5. Automatic OAuth token refresh before expiration
|
|
11
12
|
#
|
|
12
13
|
# Run this script on every container restart via .config/bashrc or .replit
|
|
13
14
|
# =============================================================================
|
|
@@ -16,9 +17,10 @@ set -e
|
|
|
16
17
|
|
|
17
18
|
# Configuration
|
|
18
19
|
WORKSPACE="/home/runner/workspace"
|
|
19
|
-
CLAUDE_PERSISTENT="${WORKSPACE}/.claude-persistent"
|
|
20
|
+
CLAUDE_PERSISTENT="${CLAUDE_CONFIG_DIR:-${WORKSPACE}/.claude-persistent}"
|
|
20
21
|
CLAUDE_LOCAL_SHARE="${WORKSPACE}/.local/share/claude"
|
|
21
22
|
CLAUDE_VERSIONS="${CLAUDE_LOCAL_SHARE}/versions"
|
|
23
|
+
AUTH_REFRESH_SCRIPT="${WORKSPACE}/scripts/claude-auth-refresh.sh"
|
|
22
24
|
|
|
23
25
|
# Target locations (ephemeral, need symlinks)
|
|
24
26
|
CLAUDE_SYMLINK="${HOME}/.claude"
|
|
@@ -39,6 +41,7 @@ mkdir -p "${CLAUDE_PERSISTENT}"
|
|
|
39
41
|
mkdir -p "${CLAUDE_VERSIONS}"
|
|
40
42
|
mkdir -p "${LOCAL_BIN}"
|
|
41
43
|
mkdir -p "${HOME}/.local/share"
|
|
44
|
+
mkdir -p "${WORKSPACE}/logs"
|
|
42
45
|
|
|
43
46
|
# =============================================================================
|
|
44
47
|
# Step 2: Create ~/.claude symlink for conversation history & credentials
|
|
@@ -63,7 +66,7 @@ fi
|
|
|
63
66
|
# =============================================================================
|
|
64
67
|
LATEST_VERSION=""
|
|
65
68
|
if [ -d "${CLAUDE_VERSIONS}" ]; then
|
|
66
|
-
LATEST_VERSION=$(ls -1 "${CLAUDE_VERSIONS}" 2>/dev/null | sort -V | tail -n1)
|
|
69
|
+
LATEST_VERSION=$(ls -1 "${CLAUDE_VERSIONS}" 2>/dev/null | grep -v '^\.' | sort -V | tail -n1)
|
|
67
70
|
fi
|
|
68
71
|
|
|
69
72
|
if [ -n "${LATEST_VERSION}" ] && [ -f "${CLAUDE_VERSIONS}/${LATEST_VERSION}" ]; then
|
|
@@ -79,31 +82,19 @@ else
|
|
|
79
82
|
# Claude not installed - install it
|
|
80
83
|
log "⚠️ Claude Code not found, installing..."
|
|
81
84
|
|
|
82
|
-
# Install Claude Code using
|
|
83
|
-
if
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
85
|
+
# Install Claude Code using the official installer
|
|
86
|
+
if curl -fsSL https://claude.ai/install.sh | bash 2>/dev/null; then
|
|
87
|
+
# After install, find the new version
|
|
88
|
+
if [ -d "${CLAUDE_VERSIONS}" ]; then
|
|
89
|
+
LATEST_VERSION=$(ls -1 "${CLAUDE_VERSIONS}" 2>/dev/null | grep -v '^\.' | sort -V | tail -n1)
|
|
90
|
+
if [ -n "${LATEST_VERSION}" ]; then
|
|
91
|
+
ln -sf "${CLAUDE_VERSIONS}/${LATEST_VERSION}" "${LOCAL_BIN}/claude"
|
|
92
|
+
log "✅ Claude Code ${LATEST_VERSION} installed"
|
|
103
93
|
fi
|
|
104
94
|
fi
|
|
105
95
|
else
|
|
106
|
-
log "❌
|
|
96
|
+
log "❌ Failed to install Claude Code"
|
|
97
|
+
log " Try running: curl -fsSL https://claude.ai/install.sh | bash"
|
|
107
98
|
fi
|
|
108
99
|
fi
|
|
109
100
|
|
|
@@ -115,11 +106,14 @@ if [[ ":$PATH:" != *":${LOCAL_BIN}:"* ]]; then
|
|
|
115
106
|
fi
|
|
116
107
|
|
|
117
108
|
# =============================================================================
|
|
118
|
-
# Step 6:
|
|
109
|
+
# Step 6: Auto-refresh OAuth token if needed
|
|
119
110
|
# =============================================================================
|
|
120
111
|
CREDENTIALS_FILE="${CLAUDE_PERSISTENT}/.credentials.json"
|
|
121
|
-
if [ -f "${CREDENTIALS_FILE}" ]; then
|
|
122
|
-
#
|
|
112
|
+
if [ -f "${CREDENTIALS_FILE}" ] && [ -f "${AUTH_REFRESH_SCRIPT}" ]; then
|
|
113
|
+
# Source the auth refresh script to get the function
|
|
114
|
+
source "${AUTH_REFRESH_SCRIPT}"
|
|
115
|
+
|
|
116
|
+
# Check and refresh if needed (this handles all the logic)
|
|
123
117
|
if command -v node &> /dev/null; then
|
|
124
118
|
AUTH_INFO=$(node -e "
|
|
125
119
|
try {
|
|
@@ -127,37 +121,70 @@ if [ -f "${CREDENTIALS_FILE}" ]; then
|
|
|
127
121
|
const oauth = creds.claudeAiOauth;
|
|
128
122
|
const apiKey = creds.primaryApiKey;
|
|
129
123
|
if (apiKey) {
|
|
130
|
-
|
|
131
|
-
console.log('apikey');
|
|
124
|
+
console.log('apikey:permanent');
|
|
132
125
|
} else if (oauth && oauth.expiresAt) {
|
|
133
|
-
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const remaining = Math.floor((oauth.expiresAt - now) / 1000 / 60 / 60);
|
|
128
|
+
const hasRefresh = oauth.refreshToken ? 'yes' : 'no';
|
|
129
|
+
console.log('oauth:' + remaining + ':' + hasRefresh);
|
|
134
130
|
}
|
|
135
|
-
} catch(e) {}
|
|
131
|
+
} catch(e) { console.log('error'); }
|
|
136
132
|
" 2>/dev/null)
|
|
137
133
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if [ "${
|
|
146
|
-
log "⚠️
|
|
134
|
+
IFS=':' read -r auth_type remaining has_refresh <<< "${AUTH_INFO}"
|
|
135
|
+
|
|
136
|
+
if [ "${auth_type}" = "apikey" ]; then
|
|
137
|
+
log "✅ Claude authentication: API key (permanent)"
|
|
138
|
+
elif [ "${auth_type}" = "oauth" ]; then
|
|
139
|
+
if [ "${remaining}" -le 0 ]; then
|
|
140
|
+
# Token expired - try to refresh
|
|
141
|
+
if [ "${has_refresh}" = "yes" ]; then
|
|
142
|
+
log "⚠️ Token expired, attempting refresh..."
|
|
143
|
+
if refresh_token 2>/dev/null; then
|
|
144
|
+
# Re-check the new expiry
|
|
145
|
+
NEW_REMAINING=$(node -e "
|
|
146
|
+
try {
|
|
147
|
+
const creds = require('${CREDENTIALS_FILE}');
|
|
148
|
+
const remaining = Math.floor((creds.claudeAiOauth.expiresAt - Date.now()) / 1000 / 60 / 60);
|
|
149
|
+
console.log(remaining);
|
|
150
|
+
} catch(e) { console.log('0'); }
|
|
151
|
+
" 2>/dev/null)
|
|
152
|
+
log "✅ Claude authentication: refreshed (${NEW_REMAINING}h remaining)"
|
|
153
|
+
else
|
|
154
|
+
log "❌ Token refresh failed - run: claude login"
|
|
155
|
+
fi
|
|
156
|
+
else
|
|
157
|
+
log "❌ Token expired (no refresh token) - run: claude login"
|
|
158
|
+
fi
|
|
159
|
+
elif [ "${remaining}" -lt 2 ]; then
|
|
160
|
+
# Less than 2 hours - refresh proactively
|
|
161
|
+
if [ "${has_refresh}" = "yes" ]; then
|
|
162
|
+
log "🔄 Token expires in ${remaining}h, refreshing..."
|
|
163
|
+
if refresh_token 2>/dev/null; then
|
|
164
|
+
NEW_REMAINING=$(node -e "
|
|
165
|
+
try {
|
|
166
|
+
const creds = require('${CREDENTIALS_FILE}');
|
|
167
|
+
const remaining = Math.floor((creds.claudeAiOauth.expiresAt - Date.now()) / 1000 / 60 / 60);
|
|
168
|
+
console.log(remaining);
|
|
169
|
+
} catch(e) { console.log('0'); }
|
|
170
|
+
" 2>/dev/null)
|
|
171
|
+
log "✅ Claude authentication: refreshed (${NEW_REMAINING}h remaining)"
|
|
172
|
+
else
|
|
173
|
+
log "⚠️ Refresh failed, ${remaining}h remaining"
|
|
174
|
+
fi
|
|
147
175
|
else
|
|
148
|
-
log "
|
|
176
|
+
log "⚠️ Claude authentication: ${remaining}h remaining (no refresh token)"
|
|
149
177
|
fi
|
|
150
178
|
else
|
|
151
|
-
log "
|
|
152
|
-
log " 💡 Tip: Run 'claude setup-token' for a long-lived token that won't expire"
|
|
179
|
+
log "✅ Claude authentication: valid (${remaining}h remaining)"
|
|
153
180
|
fi
|
|
181
|
+
elif [ "${auth_type}" = "error" ]; then
|
|
182
|
+
log "⚠️ Could not read credentials"
|
|
154
183
|
fi
|
|
155
|
-
else
|
|
156
|
-
log "✅ Claude credentials file exists (persisted in workspace)"
|
|
157
184
|
fi
|
|
158
|
-
|
|
185
|
+
elif [ ! -f "${CREDENTIALS_FILE}" ]; then
|
|
159
186
|
log "⚠️ No Claude credentials found. Run 'claude login' to authenticate"
|
|
160
|
-
log " 💡 Tip: Run 'claude setup-token' for a long-lived token
|
|
187
|
+
log " 💡 Tip: Run 'claude setup-token' for a long-lived token"
|
|
161
188
|
fi
|
|
162
189
|
|
|
163
190
|
# =============================================================================
|