replit-tools 1.0.4 → 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 +64 -22
- package/index.js +463 -291
- package/package.json +1 -1
- package/scripts/claude-auth-refresh.sh +221 -0
- package/scripts/setup-claude-code.sh +74 -47
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
|
# =============================================================================
|