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 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. **Launch the session picker** so you can start working immediately
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 Code already installed (1.0.17)
47
- Codex CLI already installed
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 → /workspace/.claude-persistent/
105
- ~/.codex → /workspace/.codex-persistent/
128
+ ~/.claude → /workspace/.claude-persistent/
129
+ ~/.codex → /workspace/.codex-persistent/
106
130
  ~/.local/share/claude → /workspace/.local/share/claude/
107
- ~/.local/bin/claude → /workspace/.local/share/claude/versions/X.X.X
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
- - **Existing persistent config** - Uses your existing `.claude-persistent/` if present
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
- ### Fix authentication permanently
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 doesn't expire (OAuth tokens expire every ~24h).
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
- └── claude-session-manager.sh
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 expired
255
+ ### Auth keeps expiring
256
+
257
+ The auto-refresh should handle this, but if it fails:
229
258
 
230
259
  ```bash
231
- claude login
232
- # Or for permanent fix:
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
- try {
209
- const installEnv = {
210
- ...process.env,
211
- CLAUDE_CONFIG_DIR: claudePersistentDir,
212
- CLAUDE_WORKSPACE_DIR: claudePersistentDir
213
- };
214
- execSync('curl -fsSL https://claude.ai/install.sh | bash', {
215
- stdio: 'inherit',
216
- shell: '/bin/bash',
217
- env: installEnv,
218
- timeout: 120000 // 2 minute timeout
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
- } catch (err) {
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
- try {
238
- const installEnv = {
239
- ...process.env,
240
- CODEX_HOME: codexPersistentDir
241
- };
242
- execSync('npm i -g @openai/codex', {
243
- stdio: 'inherit',
244
- shell: '/bin/bash',
245
- env: installEnv,
246
- timeout: 120000 // 2 minute timeout
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
- } catch (err) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replit-tools",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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
  # =============================================================================