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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replit-tools",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "DATA Tools - One command to set up Claude Code and Codex CLI on Replit with full persistence",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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 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
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 "❌ npm not found, cannot install Claude Code"
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: Verify authentication
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
- # Check if credentials are valid (not expired)
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
- // Long-lived API key - doesn't expire
131
- console.log('apikey');
124
+ console.log('apikey:permanent');
132
125
  } else if (oauth && oauth.expiresAt) {
133
- console.log(oauth.expiresAt);
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
- 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"
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 "Claude authentication: valid (${HOURS_LEFT}h remaining)"
176
+ log "⚠️ Claude authentication: ${remaining}h remaining (no refresh token)"
149
177
  fi
150
178
  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"
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
- else
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 that won't expire"
187
+ log " 💡 Tip: Run 'claude setup-token' for a long-lived token"
161
188
  fi
162
189
 
163
190
  # =============================================================================