replit-tools 1.0.5 → 1.0.6
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 +49 -17
- package/index.js +1 -1
- 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,6 +1,6 @@
|
|
|
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
|
|
|
@@ -16,7 +16,8 @@ That's it. The installer will:
|
|
|
16
16
|
2. **Install OpenAI Codex CLI** (if not already installed)
|
|
17
17
|
3. **Detect existing config** and preserve your data
|
|
18
18
|
4. **Set up persistence** so everything survives restarts
|
|
19
|
-
5. **
|
|
19
|
+
5. **Auto-refresh OAuth tokens** before they expire
|
|
20
|
+
6. **Launch the session picker** so you can start working immediately
|
|
20
21
|
|
|
21
22
|
## What Gets Installed
|
|
22
23
|
|
|
@@ -38,15 +39,36 @@ Both are installed only if not already present. Existing installations are prese
|
|
|
38
39
|
| Bash history | `.persistent-home/` | Yes |
|
|
39
40
|
| Per-terminal sessions | `.claude-sessions/` | Yes |
|
|
40
41
|
|
|
42
|
+
## Automatic Token Refresh
|
|
43
|
+
|
|
44
|
+
Claude OAuth tokens expire every **8-12 hours**. DATA Tools automatically refreshes them:
|
|
45
|
+
|
|
46
|
+
- **On every shell start**: Checks token expiry and refreshes if < 2 hours remaining
|
|
47
|
+
- **When expired**: Attempts automatic refresh using the stored refresh token
|
|
48
|
+
- **Transparent**: You'll see `🔄 Token expires in 1h, refreshing...` then `✅ Token refreshed (11h remaining)`
|
|
49
|
+
|
|
50
|
+
This means you can leave overnight and come back to a working session - no more `claude login` every morning.
|
|
51
|
+
|
|
52
|
+
### Manual Token Commands
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Check token status
|
|
56
|
+
/home/runner/workspace/scripts/claude-auth-refresh.sh --status
|
|
57
|
+
|
|
58
|
+
# Force refresh now
|
|
59
|
+
/home/runner/workspace/scripts/claude-auth-refresh.sh --force
|
|
60
|
+
|
|
61
|
+
# Or use a permanent API token (never expires)
|
|
62
|
+
claude setup-token
|
|
63
|
+
```
|
|
64
|
+
|
|
41
65
|
## The Session Picker
|
|
42
66
|
|
|
43
67
|
After installation (and on every new shell), you'll see:
|
|
44
68
|
|
|
45
69
|
```
|
|
46
|
-
✅ Claude
|
|
47
|
-
✅
|
|
48
|
-
|
|
49
|
-
✅ Claude authentication: valid (23h remaining)
|
|
70
|
+
✅ Claude authentication: valid (11h remaining)
|
|
71
|
+
✅ Claude Code ready: 2.0.71 (Claude Code)
|
|
50
72
|
|
|
51
73
|
╭─────────────────────────────────────────────────────────╮
|
|
52
74
|
│ Claude Session Manager │
|
|
@@ -101,10 +123,10 @@ Press `c` to continue YOUR terminal's last session. Other terminals are unaffect
|
|
|
101
123
|
The installer creates symlinks from ephemeral locations to persistent workspace storage:
|
|
102
124
|
|
|
103
125
|
```
|
|
104
|
-
~/.claude
|
|
105
|
-
~/.codex
|
|
126
|
+
~/.claude → /workspace/.claude-persistent/
|
|
127
|
+
~/.codex → /workspace/.codex-persistent/
|
|
106
128
|
~/.local/share/claude → /workspace/.local/share/claude/
|
|
107
|
-
~/.local/bin/claude
|
|
129
|
+
~/.local/bin/claude → /workspace/.local/share/claude/versions/X.X.X
|
|
108
130
|
```
|
|
109
131
|
|
|
110
132
|
Three layers ensure setup runs on every restart:
|
|
@@ -117,7 +139,8 @@ Three layers ensure setup runs on every restart:
|
|
|
117
139
|
The installer checks for:
|
|
118
140
|
|
|
119
141
|
- **`CLAUDE_CONFIG_DIR`** - Respects custom Claude config directory if set in Replit Secrets
|
|
120
|
-
-
|
|
142
|
+
- **`CODEX_HOME`** - Respects custom Codex config directory
|
|
143
|
+
- **Existing persistent config** - Uses your existing config if present
|
|
121
144
|
- **Replit Secrets** - Detects `ANTHROPIC_API_KEY` and `OPENAI_API_KEY`
|
|
122
145
|
- **Existing installations** - Won't reinstall Claude or Codex if already present
|
|
123
146
|
- **Existing data in ~/.claude** - Moves it to persistent storage instead of overwriting
|
|
@@ -187,13 +210,13 @@ export CLAUDE_NO_PROMPT=true
|
|
|
187
210
|
|
|
188
211
|
Add to `.config/bashrc` to make permanent.
|
|
189
212
|
|
|
190
|
-
###
|
|
213
|
+
### Use a permanent API token
|
|
191
214
|
|
|
192
215
|
```bash
|
|
193
216
|
claude setup-token
|
|
194
217
|
```
|
|
195
218
|
|
|
196
|
-
Creates a long-lived API token that
|
|
219
|
+
Creates a long-lived API token that never expires (recommended for unattended use).
|
|
197
220
|
|
|
198
221
|
## Files Created
|
|
199
222
|
|
|
@@ -205,9 +228,11 @@ workspace/
|
|
|
205
228
|
├── .local/share/claude/ # Claude binary versions
|
|
206
229
|
├── .persistent-home/ # Bash history
|
|
207
230
|
├── .config/bashrc # Shell startup config
|
|
231
|
+
├── logs/ # Auth refresh logs
|
|
208
232
|
├── scripts/
|
|
209
|
-
│ ├── setup-claude-code.sh
|
|
210
|
-
│
|
|
233
|
+
│ ├── setup-claude-code.sh # Main setup script
|
|
234
|
+
│ ├── claude-session-manager.sh # Interactive session picker
|
|
235
|
+
│ └── claude-auth-refresh.sh # OAuth token auto-refresh
|
|
211
236
|
└── .gitignore # Updated to ignore credential dirs
|
|
212
237
|
```
|
|
213
238
|
|
|
@@ -225,11 +250,18 @@ source /home/runner/workspace/scripts/setup-claude-code.sh
|
|
|
225
250
|
source /home/runner/workspace/.config/bashrc
|
|
226
251
|
```
|
|
227
252
|
|
|
228
|
-
### Auth
|
|
253
|
+
### Auth keeps expiring
|
|
254
|
+
|
|
255
|
+
The auto-refresh should handle this, but if it fails:
|
|
229
256
|
|
|
230
257
|
```bash
|
|
231
|
-
|
|
232
|
-
|
|
258
|
+
# Check why refresh failed
|
|
259
|
+
cat /home/runner/workspace/logs/auth-refresh.log
|
|
260
|
+
|
|
261
|
+
# Manual refresh
|
|
262
|
+
/home/runner/workspace/scripts/claude-auth-refresh.sh --force
|
|
263
|
+
|
|
264
|
+
# Or use permanent token (recommended)
|
|
233
265
|
claude setup-token
|
|
234
266
|
```
|
|
235
267
|
|
package/index.js
CHANGED
|
@@ -375,7 +375,7 @@ function main() {
|
|
|
375
375
|
console.log('');
|
|
376
376
|
console.log('📝 Installing scripts...');
|
|
377
377
|
|
|
378
|
-
const scripts = ['setup-claude-code.sh', 'claude-session-manager.sh'];
|
|
378
|
+
const scripts = ['setup-claude-code.sh', 'claude-session-manager.sh', 'claude-auth-refresh.sh'];
|
|
379
379
|
scripts.forEach(script => {
|
|
380
380
|
const srcPath = path.join(scriptsDir, script);
|
|
381
381
|
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
|
# =============================================================================
|