replit-tools 1.0.0
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 +150 -0
- package/index.js +129 -0
- package/install.sh +419 -0
- package/package.json +30 -0
- package/scripts/claude-session-manager.sh +344 -0
- package/scripts/setup-claude-code.sh +171 -0
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# replit-claude-persist
|
|
2
|
+
|
|
3
|
+
**Persist Claude Code sessions, authentication, and history across Replit container restarts.**
|
|
4
|
+
|
|
5
|
+
When Replit containers restart, everything outside `/home/runner/workspace/` is wiped - including your Claude conversations, auth tokens, and installed binaries. This tool fixes that.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Session Persistence** - Conversations survive container restarts
|
|
10
|
+
- **Interactive Session Picker** - Choose which session to resume on shell start
|
|
11
|
+
- **Multi-Terminal Support** - Each terminal tracks its own session
|
|
12
|
+
- **Auth Persistence** - Keep your Claude authentication working
|
|
13
|
+
- **Binary Caching** - Claude binary persists (faster startup)
|
|
14
|
+
- **Bash History** - Command history survives restarts too
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
### Option 1: npx (easiest)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx replit-claude-persist
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Option 2: curl
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
curl -fsSL https://raw.githubusercontent.com/stevemoraco/DATAtools/main/install.sh | bash
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Option 3: Manual
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g replit-claude-persist
|
|
34
|
+
replit-claude-persist
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## What You'll See
|
|
38
|
+
|
|
39
|
+
After installation, opening a new shell shows:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
✅ Claude authentication: valid (23h remaining)
|
|
43
|
+
✅ Claude Code ready: 2.0.71 (Claude Code)
|
|
44
|
+
|
|
45
|
+
╭─────────────────────────────────────────────────────────╮
|
|
46
|
+
│ Claude Session Manager │
|
|
47
|
+
╰─────────────────────────────────────────────────────────╯
|
|
48
|
+
(2 Claude instance(s) running in other terminals)
|
|
49
|
+
|
|
50
|
+
[c] Continue last session for this terminal
|
|
51
|
+
└─ b3dcb95c...
|
|
52
|
+
[r] Resume a specific session (pick from list)
|
|
53
|
+
[n] Start new session
|
|
54
|
+
[s] Skip - just give me a shell
|
|
55
|
+
|
|
56
|
+
Choice [c/r/n/s]: _
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Press `r` to see detailed session info:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
63
|
+
[1]
|
|
64
|
+
ID: b3dcb95c-cebb-4082-b671-988c8d36578e
|
|
65
|
+
Messages: 237 | Size: 912.1KB
|
|
66
|
+
Active: 2m ago
|
|
67
|
+
Started: 2026-01-18 18:22:12 UTC
|
|
68
|
+
First: "Can you help me fix this bug..."
|
|
69
|
+
Latest: "Thanks, that worked!"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## How It Works
|
|
73
|
+
|
|
74
|
+
The tool creates symlinks from ephemeral locations to persistent workspace storage:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
~/.claude → /workspace/.claude-persistent/
|
|
78
|
+
~/.codex → /workspace/.codex-persistent/
|
|
79
|
+
~/.local/bin/ → /workspace/.local/share/claude/versions/
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
On every shell start, `.config/bashrc` ensures symlinks exist and shows the session picker.
|
|
83
|
+
|
|
84
|
+
## Commands
|
|
85
|
+
|
|
86
|
+
| Command | Description |
|
|
87
|
+
|---------|-------------|
|
|
88
|
+
| `cr` | Continue last session |
|
|
89
|
+
| `claude-menu` | Show session picker again |
|
|
90
|
+
| `claude-new` | Start fresh session |
|
|
91
|
+
| `claude-pick` | Claude's built-in picker |
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
### Disable the menu
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
export CLAUDE_NO_PROMPT=true
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Add to `.config/bashrc` to make permanent.
|
|
102
|
+
|
|
103
|
+
### Fix auth permanently
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
claude setup-token
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Creates a long-lived token that doesn't expire.
|
|
110
|
+
|
|
111
|
+
## Files Created
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
workspace/
|
|
115
|
+
├── .claude-persistent/ # Conversations, credentials
|
|
116
|
+
├── .codex-persistent/ # Codex CLI data
|
|
117
|
+
├── .claude-sessions/ # Per-terminal session tracking
|
|
118
|
+
├── .local/share/claude/ # Claude binary versions
|
|
119
|
+
├── .persistent-home/ # Bash history
|
|
120
|
+
├── .config/bashrc # Shell startup config
|
|
121
|
+
└── scripts/
|
|
122
|
+
├── setup-claude-code.sh
|
|
123
|
+
└── claude-session-manager.sh
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Troubleshooting
|
|
127
|
+
|
|
128
|
+
### Menu not appearing
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
source /home/runner/workspace/.config/bashrc
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Auth expired
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
claude login
|
|
138
|
+
# Or for permanent fix:
|
|
139
|
+
claude setup-token
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Symlinks broken
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
source /home/runner/workspace/scripts/setup-claude-code.sh
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const WORKSPACE = '/home/runner/workspace';
|
|
8
|
+
|
|
9
|
+
// Check if we're on Replit
|
|
10
|
+
if (!fs.existsSync(WORKSPACE)) {
|
|
11
|
+
console.error('❌ This tool must be run on Replit');
|
|
12
|
+
console.error(' /home/runner/workspace not found');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log('');
|
|
17
|
+
console.log('╭─────────────────────────────────────────────────────────╮');
|
|
18
|
+
console.log('│ Replit Claude Persistence Installer │');
|
|
19
|
+
console.log('╰─────────────────────────────────────────────────────────╯');
|
|
20
|
+
console.log('');
|
|
21
|
+
|
|
22
|
+
// Create directories
|
|
23
|
+
const dirs = [
|
|
24
|
+
'.claude-persistent',
|
|
25
|
+
'.codex-persistent',
|
|
26
|
+
'.claude-sessions',
|
|
27
|
+
'.local/share/claude/versions',
|
|
28
|
+
'.persistent-home',
|
|
29
|
+
'.config',
|
|
30
|
+
'scripts',
|
|
31
|
+
'logs'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
console.log('📁 Creating directories...');
|
|
35
|
+
dirs.forEach(dir => {
|
|
36
|
+
const fullPath = path.join(WORKSPACE, dir);
|
|
37
|
+
if (!fs.existsSync(fullPath)) {
|
|
38
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Copy scripts from the package
|
|
43
|
+
const scriptsDir = path.join(__dirname, 'scripts');
|
|
44
|
+
const targetScriptsDir = path.join(WORKSPACE, 'scripts');
|
|
45
|
+
|
|
46
|
+
console.log('📝 Installing scripts...');
|
|
47
|
+
|
|
48
|
+
// Read and write each script
|
|
49
|
+
const scripts = ['setup-claude-code.sh', 'claude-session-manager.sh'];
|
|
50
|
+
scripts.forEach(script => {
|
|
51
|
+
const srcPath = path.join(scriptsDir, script);
|
|
52
|
+
const destPath = path.join(targetScriptsDir, script);
|
|
53
|
+
|
|
54
|
+
if (fs.existsSync(srcPath)) {
|
|
55
|
+
fs.copyFileSync(srcPath, destPath);
|
|
56
|
+
fs.chmodSync(destPath, '755');
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Create/update .config/bashrc
|
|
61
|
+
console.log('📝 Creating .config/bashrc...');
|
|
62
|
+
const bashrcContent = `#!/bin/bash
|
|
63
|
+
# Replit Claude Persistence - Auto-generated bashrc
|
|
64
|
+
|
|
65
|
+
# Claude Code Setup
|
|
66
|
+
SETUP_SCRIPT="/home/runner/workspace/scripts/setup-claude-code.sh"
|
|
67
|
+
[ -f "\${SETUP_SCRIPT}" ] && source "\${SETUP_SCRIPT}"
|
|
68
|
+
|
|
69
|
+
# Codex Persistence
|
|
70
|
+
CODEX_PERSISTENT="/home/runner/workspace/.codex-persistent"
|
|
71
|
+
mkdir -p "\${CODEX_PERSISTENT}"
|
|
72
|
+
[ ! -L "\${HOME}/.codex" ] && ln -sf "\${CODEX_PERSISTENT}" "\${HOME}/.codex"
|
|
73
|
+
|
|
74
|
+
# Bash History Persistence
|
|
75
|
+
PERSISTENT_HOME="/home/runner/workspace/.persistent-home"
|
|
76
|
+
mkdir -p "\${PERSISTENT_HOME}"
|
|
77
|
+
export HISTFILE="\${PERSISTENT_HOME}/.bash_history"
|
|
78
|
+
export HISTSIZE=10000
|
|
79
|
+
export HISTFILESIZE=20000
|
|
80
|
+
export HISTCONTROL=ignoredups
|
|
81
|
+
[ -f "\${HISTFILE}" ] && history -r "\${HISTFILE}"
|
|
82
|
+
|
|
83
|
+
# Session Manager (interactive menu)
|
|
84
|
+
SESSION_MANAGER="/home/runner/workspace/scripts/claude-session-manager.sh"
|
|
85
|
+
[ -f "\${SESSION_MANAGER}" ] && source "\${SESSION_MANAGER}"
|
|
86
|
+
|
|
87
|
+
# Aliases
|
|
88
|
+
alias cr='claude -c --dangerously-skip-permissions'
|
|
89
|
+
alias claude-resume='claude -c --dangerously-skip-permissions'
|
|
90
|
+
alias claude-pick='claude -r --dangerously-skip-permissions'
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
fs.writeFileSync(path.join(WORKSPACE, '.config/bashrc'), bashrcContent);
|
|
94
|
+
|
|
95
|
+
// Update .replit
|
|
96
|
+
console.log('📝 Updating .replit configuration...');
|
|
97
|
+
const replitPath = path.join(WORKSPACE, '.replit');
|
|
98
|
+
const onBootLine = 'onBoot = "source /home/runner/workspace/scripts/setup-claude-code.sh 2>/dev/null || true"';
|
|
99
|
+
|
|
100
|
+
if (fs.existsSync(replitPath)) {
|
|
101
|
+
let content = fs.readFileSync(replitPath, 'utf8');
|
|
102
|
+
if (!content.includes('setup-claude-code.sh')) {
|
|
103
|
+
content += '\n\n# Claude persistence (added by installer)\n' + onBootLine + '\n';
|
|
104
|
+
fs.writeFileSync(replitPath, content);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
fs.writeFileSync(replitPath, '# Claude persistence\n' + onBootLine + '\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log('✅ Installation complete!');
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log('What happens now:');
|
|
114
|
+
console.log(' • New shells will show the Claude session picker');
|
|
115
|
+
console.log(' • Your conversations persist across container restarts');
|
|
116
|
+
console.log(' • Claude binary is cached (faster startup)');
|
|
117
|
+
console.log(' • Bash history is preserved');
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log('To test, open a new shell or run:');
|
|
120
|
+
console.log(' source ~/.config/bashrc');
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log('Options:');
|
|
123
|
+
console.log(" Press 'c' - Continue last session");
|
|
124
|
+
console.log(" Press 'r' - Pick from session list");
|
|
125
|
+
console.log(" Press 'n' - New session");
|
|
126
|
+
console.log(" Press 's' - Skip (just a shell)");
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log('To disable the menu: export CLAUDE_NO_PROMPT=true');
|
|
129
|
+
console.log('');
|
package/install.sh
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# Replit Claude Persistence Installer
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# One-line install:
|
|
6
|
+
# curl -fsSL https://raw.githubusercontent.com/YOUR_USERNAME/replit-claude-persist/main/install.sh | bash
|
|
7
|
+
#
|
|
8
|
+
# Or with npx:
|
|
9
|
+
# npx replit-claude-persist
|
|
10
|
+
# =============================================================================
|
|
11
|
+
|
|
12
|
+
set -e
|
|
13
|
+
|
|
14
|
+
WORKSPACE="/home/runner/workspace"
|
|
15
|
+
GREEN='\033[0;32m'
|
|
16
|
+
YELLOW='\033[1;33m'
|
|
17
|
+
RED='\033[0;31m'
|
|
18
|
+
NC='\033[0m' # No Color
|
|
19
|
+
|
|
20
|
+
echo ""
|
|
21
|
+
echo "╭─────────────────────────────────────────────────────────╮"
|
|
22
|
+
echo "│ Replit Claude Persistence Installer │"
|
|
23
|
+
echo "╰─────────────────────────────────────────────────────────╯"
|
|
24
|
+
echo ""
|
|
25
|
+
|
|
26
|
+
# Check we're on Replit
|
|
27
|
+
if [ ! -d "/home/runner/workspace" ]; then
|
|
28
|
+
echo -e "${RED}ERROR: This script must be run on Replit${NC}"
|
|
29
|
+
echo " /home/runner/workspace not found"
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
cd "$WORKSPACE"
|
|
34
|
+
|
|
35
|
+
echo "📁 Creating directories..."
|
|
36
|
+
mkdir -p .claude-persistent
|
|
37
|
+
mkdir -p .codex-persistent
|
|
38
|
+
mkdir -p .claude-sessions
|
|
39
|
+
mkdir -p .local/share/claude/versions
|
|
40
|
+
mkdir -p .persistent-home
|
|
41
|
+
mkdir -p .config
|
|
42
|
+
mkdir -p scripts
|
|
43
|
+
mkdir -p logs
|
|
44
|
+
|
|
45
|
+
echo "📝 Installing scripts..."
|
|
46
|
+
|
|
47
|
+
# setup-claude-code.sh
|
|
48
|
+
cat > scripts/setup-claude-code.sh << 'SCRIPT_EOF'
|
|
49
|
+
#!/bin/bash
|
|
50
|
+
# Claude Code Setup Script for Replit
|
|
51
|
+
set -e
|
|
52
|
+
|
|
53
|
+
WORKSPACE="/home/runner/workspace"
|
|
54
|
+
CLAUDE_PERSISTENT="${WORKSPACE}/.claude-persistent"
|
|
55
|
+
CLAUDE_LOCAL_SHARE="${WORKSPACE}/.local/share/claude"
|
|
56
|
+
CLAUDE_VERSIONS="${CLAUDE_LOCAL_SHARE}/versions"
|
|
57
|
+
CLAUDE_SYMLINK="${HOME}/.claude"
|
|
58
|
+
LOCAL_BIN="${HOME}/.local/bin"
|
|
59
|
+
LOCAL_SHARE_CLAUDE="${HOME}/.local/share/claude"
|
|
60
|
+
|
|
61
|
+
log() {
|
|
62
|
+
if [[ $- == *i* ]]; then
|
|
63
|
+
echo "$1"
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
mkdir -p "${CLAUDE_PERSISTENT}"
|
|
68
|
+
mkdir -p "${CLAUDE_VERSIONS}"
|
|
69
|
+
mkdir -p "${LOCAL_BIN}"
|
|
70
|
+
mkdir -p "${HOME}/.local/share"
|
|
71
|
+
|
|
72
|
+
# Symlink ~/.claude
|
|
73
|
+
if [ ! -L "${CLAUDE_SYMLINK}" ] || [ "$(readlink -f "${CLAUDE_SYMLINK}")" != "${CLAUDE_PERSISTENT}" ]; then
|
|
74
|
+
rm -rf "${CLAUDE_SYMLINK}" 2>/dev/null || true
|
|
75
|
+
ln -sf "${CLAUDE_PERSISTENT}" "${CLAUDE_SYMLINK}"
|
|
76
|
+
log "✅ Claude history symlink: ~/.claude -> ${CLAUDE_PERSISTENT}"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Symlink ~/.local/share/claude
|
|
80
|
+
if [ ! -L "${LOCAL_SHARE_CLAUDE}" ] || [ "$(readlink -f "${LOCAL_SHARE_CLAUDE}")" != "${CLAUDE_LOCAL_SHARE}" ]; then
|
|
81
|
+
rm -rf "${LOCAL_SHARE_CLAUDE}" 2>/dev/null || true
|
|
82
|
+
ln -sf "${CLAUDE_LOCAL_SHARE}" "${LOCAL_SHARE_CLAUDE}"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Find and symlink binary
|
|
86
|
+
LATEST_VERSION=""
|
|
87
|
+
if [ -d "${CLAUDE_VERSIONS}" ]; then
|
|
88
|
+
LATEST_VERSION=$(ls -1 "${CLAUDE_VERSIONS}" 2>/dev/null | sort -V | tail -n1)
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
if [ -n "${LATEST_VERSION}" ] && [ -f "${CLAUDE_VERSIONS}/${LATEST_VERSION}" ]; then
|
|
92
|
+
CLAUDE_BINARY="${CLAUDE_VERSIONS}/${LATEST_VERSION}"
|
|
93
|
+
if [ ! -L "${LOCAL_BIN}/claude" ] || [ "$(readlink -f "${LOCAL_BIN}/claude")" != "${CLAUDE_BINARY}" ]; then
|
|
94
|
+
rm -f "${LOCAL_BIN}/claude" 2>/dev/null || true
|
|
95
|
+
ln -sf "${CLAUDE_BINARY}" "${LOCAL_BIN}/claude"
|
|
96
|
+
log "✅ Claude binary symlink: ~/.local/bin/claude -> ${CLAUDE_BINARY}"
|
|
97
|
+
fi
|
|
98
|
+
else
|
|
99
|
+
log "⚠️ Claude Code not found, installing..."
|
|
100
|
+
if command -v npm &> /dev/null; then
|
|
101
|
+
npm install -g @anthropic-ai/claude-code 2>/dev/null || true
|
|
102
|
+
if command -v claude &> /dev/null; then
|
|
103
|
+
INSTALLED_PATH=$(which claude)
|
|
104
|
+
if [ -f "${INSTALLED_PATH}" ] && [ ! -L "${INSTALLED_PATH}" ]; then
|
|
105
|
+
VERSION=$(claude --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1)
|
|
106
|
+
if [ -n "${VERSION}" ]; then
|
|
107
|
+
cp "${INSTALLED_PATH}" "${CLAUDE_VERSIONS}/${VERSION}"
|
|
108
|
+
chmod +x "${CLAUDE_VERSIONS}/${VERSION}"
|
|
109
|
+
ln -sf "${CLAUDE_VERSIONS}/${VERSION}" "${LOCAL_BIN}/claude"
|
|
110
|
+
log "✅ Claude Code ${VERSION} installed"
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# Ensure PATH
|
|
118
|
+
if [[ ":$PATH:" != *":${LOCAL_BIN}:"* ]]; then
|
|
119
|
+
export PATH="${LOCAL_BIN}:$PATH"
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# Check auth
|
|
123
|
+
CREDENTIALS_FILE="${CLAUDE_PERSISTENT}/.credentials.json"
|
|
124
|
+
if [ -f "${CREDENTIALS_FILE}" ]; then
|
|
125
|
+
if command -v node &> /dev/null; then
|
|
126
|
+
AUTH_INFO=$(node -e "
|
|
127
|
+
try {
|
|
128
|
+
const creds = require('${CREDENTIALS_FILE}');
|
|
129
|
+
const oauth = creds.claudeAiOauth;
|
|
130
|
+
const apiKey = creds.primaryApiKey;
|
|
131
|
+
if (apiKey) console.log('apikey');
|
|
132
|
+
else if (oauth && oauth.expiresAt) console.log(oauth.expiresAt);
|
|
133
|
+
} catch(e) {}
|
|
134
|
+
" 2>/dev/null)
|
|
135
|
+
|
|
136
|
+
if [ "${AUTH_INFO}" = "apikey" ]; then
|
|
137
|
+
log "✅ Claude authentication: long-lived token (no expiration)"
|
|
138
|
+
elif [ -n "${AUTH_INFO}" ]; then
|
|
139
|
+
CURRENT_TIME=$(node -e "console.log(Date.now())" 2>/dev/null)
|
|
140
|
+
if [ -n "${CURRENT_TIME}" ] && [ "${AUTH_INFO}" -gt "${CURRENT_TIME}" ]; then
|
|
141
|
+
HOURS_LEFT=$(node -e "console.log(Math.floor((${AUTH_INFO} - ${CURRENT_TIME}) / 1000 / 60 / 60))" 2>/dev/null)
|
|
142
|
+
if [ "${HOURS_LEFT}" -lt 2 ]; then
|
|
143
|
+
log "⚠️ Claude authentication: expires in ${HOURS_LEFT}h"
|
|
144
|
+
else
|
|
145
|
+
log "✅ Claude authentication: valid (${HOURS_LEFT}h remaining)"
|
|
146
|
+
fi
|
|
147
|
+
else
|
|
148
|
+
log "⚠️ Claude authentication: expired, run 'claude login'"
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
else
|
|
153
|
+
log "⚠️ No Claude credentials. Run 'claude login'"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
if [[ $- == *i* ]] && [ -n "${LATEST_VERSION}" ]; then
|
|
157
|
+
if command -v claude &> /dev/null; then
|
|
158
|
+
CLAUDE_VERSION=$(claude --version 2>/dev/null | head -1)
|
|
159
|
+
log "✅ Claude Code ready: ${CLAUDE_VERSION}"
|
|
160
|
+
fi
|
|
161
|
+
fi
|
|
162
|
+
SCRIPT_EOF
|
|
163
|
+
|
|
164
|
+
# claude-session-manager.sh
|
|
165
|
+
cat > scripts/claude-session-manager.sh << 'SCRIPT_EOF'
|
|
166
|
+
#!/bin/bash
|
|
167
|
+
# Claude Session Manager - Interactive Multi-Terminal Support
|
|
168
|
+
|
|
169
|
+
WORKSPACE="/home/runner/workspace"
|
|
170
|
+
SESSIONS_DIR="${WORKSPACE}/.claude-sessions"
|
|
171
|
+
LOCK_DIR="/tmp/.claude-locks"
|
|
172
|
+
|
|
173
|
+
mkdir -p "${SESSIONS_DIR}" "${LOCK_DIR}" 2>/dev/null
|
|
174
|
+
|
|
175
|
+
get_terminal_id() {
|
|
176
|
+
local tty_name=$(tty 2>/dev/null | sed 's|/dev/||' | tr '/' '-')
|
|
177
|
+
if [ -n "$tty_name" ] && [ "$tty_name" != "not" ]; then
|
|
178
|
+
echo "$tty_name"
|
|
179
|
+
else
|
|
180
|
+
echo "shell-$$"
|
|
181
|
+
fi
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
TERMINAL_ID=$(get_terminal_id)
|
|
185
|
+
STATE_FILE="${SESSIONS_DIR}/${TERMINAL_ID}.json"
|
|
186
|
+
|
|
187
|
+
get_recent_sessions() {
|
|
188
|
+
local history="${HOME}/.claude/history.jsonl"
|
|
189
|
+
local projects_dir="${HOME}/.claude/projects/-home-runner-workspace"
|
|
190
|
+
|
|
191
|
+
if [ -f "${history}" ]; then
|
|
192
|
+
node -e "
|
|
193
|
+
const fs = require('fs');
|
|
194
|
+
const path = require('path');
|
|
195
|
+
const historyFile = '${history}';
|
|
196
|
+
const projectsDir = '${projects_dir}';
|
|
197
|
+
const sessionData = new Map();
|
|
198
|
+
const lines = fs.readFileSync(historyFile, 'utf8').trim().split('\n');
|
|
199
|
+
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
try {
|
|
202
|
+
const j = JSON.parse(line);
|
|
203
|
+
if (!j.sessionId) continue;
|
|
204
|
+
if (!sessionData.has(j.sessionId)) {
|
|
205
|
+
sessionData.set(j.sessionId, {
|
|
206
|
+
id: j.sessionId, firstSeen: j.timestamp, lastSeen: j.timestamp,
|
|
207
|
+
firstPrompt: j.display || '', lastPrompt: j.display || '',
|
|
208
|
+
messageCount: 0, project: j.project || ''
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const data = sessionData.get(j.sessionId);
|
|
212
|
+
if (j.timestamp < data.firstSeen) { data.firstSeen = j.timestamp; data.firstPrompt = j.display || data.firstPrompt; }
|
|
213
|
+
if (j.timestamp > data.lastSeen) { data.lastSeen = j.timestamp; data.lastPrompt = j.display || data.lastPrompt; }
|
|
214
|
+
} catch(e) {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const [id, data] of sessionData) {
|
|
218
|
+
const jsonlPath = path.join(projectsDir, id + '.jsonl');
|
|
219
|
+
if (fs.existsSync(jsonlPath)) {
|
|
220
|
+
try {
|
|
221
|
+
const stat = fs.statSync(jsonlPath);
|
|
222
|
+
data.fileSize = stat.size;
|
|
223
|
+
data.messageCount = fs.readFileSync(jsonlPath, 'utf8').trim().split('\n').filter(l => l.trim()).length;
|
|
224
|
+
} catch(e) {}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const sorted = Array.from(sessionData.values()).sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0)).slice(0, 10);
|
|
229
|
+
sorted.forEach((s, i) => {
|
|
230
|
+
const formatTime = (ts) => { if (!ts) return 'unknown'; const d = new Date(ts); return d.toISOString().replace('T', ' ').substring(0, 19) + ' UTC'; };
|
|
231
|
+
const timeAgo = (ts) => { if (!ts) return ''; const mins = Math.round((Date.now() - ts) / 1000 / 60); if (mins < 60) return mins + 'm ago'; if (mins < 1440) return Math.round(mins/60) + 'h ago'; return Math.round(mins/1440) + 'd ago'; };
|
|
232
|
+
const sizeStr = (bytes) => { if (!bytes) return '0B'; if (bytes < 1024) return bytes + 'B'; if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + 'KB'; return (bytes/1024/1024).toFixed(1) + 'MB'; };
|
|
233
|
+
console.log('SESSION|' + (i+1));
|
|
234
|
+
console.log('ID|' + s.id);
|
|
235
|
+
console.log('MESSAGES|' + (s.messageCount || '?'));
|
|
236
|
+
console.log('SIZE|' + sizeStr(s.fileSize));
|
|
237
|
+
console.log('LAST_ACTIVE|' + timeAgo(s.lastSeen));
|
|
238
|
+
console.log('STARTED|' + formatTime(s.firstSeen));
|
|
239
|
+
console.log('FIRST_PROMPT|' + (s.firstPrompt || '').substring(0, 80).replace(/\n/g, ' ').trim());
|
|
240
|
+
console.log('LAST_PROMPT|' + (s.lastPrompt || '').substring(0, 80).replace(/\n/g, ' ').trim());
|
|
241
|
+
console.log('---');
|
|
242
|
+
});
|
|
243
|
+
" 2>/dev/null
|
|
244
|
+
fi
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
show_sessions() {
|
|
248
|
+
local data=$(get_recent_sessions)
|
|
249
|
+
[ -z "$data" ] && echo " No sessions found." && return
|
|
250
|
+
echo "$data" | while IFS='|' read -r key value; do
|
|
251
|
+
case "$key" in
|
|
252
|
+
SESSION) echo ""; echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; echo " [$value]" ;;
|
|
253
|
+
ID) echo " ID: $value" ;;
|
|
254
|
+
MESSAGES) printf " Messages: %s" "$value" ;;
|
|
255
|
+
SIZE) printf " | Size: %s\n" "$value" ;;
|
|
256
|
+
LAST_ACTIVE) echo " Active: $value" ;;
|
|
257
|
+
STARTED) echo " Started: $value" ;;
|
|
258
|
+
FIRST_PROMPT) [ -n "$value" ] && echo " First: \"$value\"" ;;
|
|
259
|
+
LAST_PROMPT) [ -n "$value" ] && echo " Latest: \"$value\"" ;;
|
|
260
|
+
esac
|
|
261
|
+
done
|
|
262
|
+
echo ""
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
count_claude_instances() { pgrep -x "claude" 2>/dev/null | wc -l; }
|
|
266
|
+
|
|
267
|
+
save_session_state() {
|
|
268
|
+
local session_id="$1"
|
|
269
|
+
local flags="${2:---dangerously-skip-permissions}"
|
|
270
|
+
mkdir -p "${SESSIONS_DIR}"
|
|
271
|
+
cat > "${STATE_FILE}" << EOF
|
|
272
|
+
{"sessionId": "${session_id}", "flags": "${flags}", "terminalId": "${TERMINAL_ID}", "timestamp": $(date +%s)}
|
|
273
|
+
EOF
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
get_terminal_last_session() {
|
|
277
|
+
[ -f "${STATE_FILE}" ] && node -e "try{console.log(require('${STATE_FILE}').sessionId||'')}catch(e){}" 2>/dev/null
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
claude_prompt() {
|
|
281
|
+
[[ $- != *i* ]] && return 0
|
|
282
|
+
command -v claude &>/dev/null || return 0
|
|
283
|
+
|
|
284
|
+
local running=$(count_claude_instances)
|
|
285
|
+
local last_session=$(get_terminal_last_session)
|
|
286
|
+
|
|
287
|
+
echo ""
|
|
288
|
+
echo "╭─────────────────────────────────────────────────────────╮"
|
|
289
|
+
echo "│ Claude Session Manager │"
|
|
290
|
+
echo "╰─────────────────────────────────────────────────────────╯"
|
|
291
|
+
[ "$running" -gt 0 ] && echo " ($running Claude instance(s) running in other terminals)"
|
|
292
|
+
echo ""
|
|
293
|
+
echo " [c] Continue last session for this terminal"
|
|
294
|
+
[ -n "$last_session" ] && echo " └─ ${last_session:0:8}..."
|
|
295
|
+
echo " [r] Resume a specific session (pick from list)"
|
|
296
|
+
echo " [n] Start new session"
|
|
297
|
+
echo " [s] Skip - just give me a shell"
|
|
298
|
+
echo ""
|
|
299
|
+
|
|
300
|
+
local choice
|
|
301
|
+
read -t 30 -n 1 -p " Choice [c/r/n/s]: " choice
|
|
302
|
+
echo ""
|
|
303
|
+
|
|
304
|
+
case "$choice" in
|
|
305
|
+
c|C|"")
|
|
306
|
+
if [ -n "$last_session" ]; then
|
|
307
|
+
echo ""; echo " Resuming session ${last_session:0:8}..."
|
|
308
|
+
claude -r "$last_session" --dangerously-skip-permissions
|
|
309
|
+
save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)"
|
|
310
|
+
else
|
|
311
|
+
echo ""; echo " No previous session, starting new..."
|
|
312
|
+
claude --dangerously-skip-permissions
|
|
313
|
+
save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)"
|
|
314
|
+
fi ;;
|
|
315
|
+
r|R)
|
|
316
|
+
echo ""; echo " Recent Sessions"; show_sessions
|
|
317
|
+
local session_ids=$(get_recent_sessions | grep "^ID|" | cut -d'|' -f2)
|
|
318
|
+
read -p " Enter number (or 'q' to cancel): " session_num
|
|
319
|
+
[ "$session_num" = "q" ] || [ -z "$session_num" ] && echo " Cancelled." && return 0
|
|
320
|
+
local selected_id=$(echo "$session_ids" | sed -n "${session_num}p")
|
|
321
|
+
if [ -n "$selected_id" ]; then
|
|
322
|
+
echo ""; echo " Resuming session: $selected_id"
|
|
323
|
+
claude -r "$selected_id" --dangerously-skip-permissions
|
|
324
|
+
save_session_state "$selected_id"
|
|
325
|
+
else echo " Invalid selection."; fi ;;
|
|
326
|
+
n|N)
|
|
327
|
+
echo ""; echo " Starting new Claude session..."
|
|
328
|
+
claude --dangerously-skip-permissions
|
|
329
|
+
save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)" ;;
|
|
330
|
+
s|S) echo ""; echo " Okay, just a shell. Type 'claude' or 'cr' when ready." ;;
|
|
331
|
+
*) echo ""; echo " Unknown option. Type 'claude' to start manually." ;;
|
|
332
|
+
esac
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
alias cr='claude -c --dangerously-skip-permissions'
|
|
336
|
+
alias claude-resume='claude -c --dangerously-skip-permissions'
|
|
337
|
+
alias claude-pick='claude -r --dangerously-skip-permissions'
|
|
338
|
+
alias claude-new='claude --dangerously-skip-permissions'
|
|
339
|
+
alias claude-menu='claude_prompt'
|
|
340
|
+
|
|
341
|
+
export -f get_recent_sessions save_session_state
|
|
342
|
+
export TERMINAL_ID
|
|
343
|
+
|
|
344
|
+
[ "${CLAUDE_NO_PROMPT}" != "true" ] && claude_prompt
|
|
345
|
+
SCRIPT_EOF
|
|
346
|
+
|
|
347
|
+
chmod +x scripts/setup-claude-code.sh
|
|
348
|
+
chmod +x scripts/claude-session-manager.sh
|
|
349
|
+
|
|
350
|
+
# Create .config/bashrc
|
|
351
|
+
echo "📝 Creating .config/bashrc..."
|
|
352
|
+
cat > .config/bashrc << 'BASHRC_EOF'
|
|
353
|
+
#!/bin/bash
|
|
354
|
+
# Replit Claude Persistence - Auto-generated bashrc
|
|
355
|
+
|
|
356
|
+
# Claude Code Setup
|
|
357
|
+
SETUP_SCRIPT="/home/runner/workspace/scripts/setup-claude-code.sh"
|
|
358
|
+
[ -f "${SETUP_SCRIPT}" ] && source "${SETUP_SCRIPT}"
|
|
359
|
+
|
|
360
|
+
# Codex Persistence
|
|
361
|
+
CODEX_PERSISTENT="/home/runner/workspace/.codex-persistent"
|
|
362
|
+
mkdir -p "${CODEX_PERSISTENT}"
|
|
363
|
+
[ ! -L "${HOME}/.codex" ] && ln -sf "${CODEX_PERSISTENT}" "${HOME}/.codex"
|
|
364
|
+
|
|
365
|
+
# Bash History Persistence
|
|
366
|
+
PERSISTENT_HOME="/home/runner/workspace/.persistent-home"
|
|
367
|
+
mkdir -p "${PERSISTENT_HOME}"
|
|
368
|
+
export HISTFILE="${PERSISTENT_HOME}/.bash_history"
|
|
369
|
+
export HISTSIZE=10000
|
|
370
|
+
export HISTFILESIZE=20000
|
|
371
|
+
export HISTCONTROL=ignoredups
|
|
372
|
+
[ -f "${HISTFILE}" ] && history -r "${HISTFILE}"
|
|
373
|
+
|
|
374
|
+
# Session Manager (interactive menu)
|
|
375
|
+
SESSION_MANAGER="/home/runner/workspace/scripts/claude-session-manager.sh"
|
|
376
|
+
[ -f "${SESSION_MANAGER}" ] && source "${SESSION_MANAGER}"
|
|
377
|
+
|
|
378
|
+
# Aliases
|
|
379
|
+
alias cr='claude -c --dangerously-skip-permissions'
|
|
380
|
+
alias claude-resume='claude -c --dangerously-skip-permissions'
|
|
381
|
+
alias claude-pick='claude -r --dangerously-skip-permissions'
|
|
382
|
+
BASHRC_EOF
|
|
383
|
+
|
|
384
|
+
# Update .replit if it exists
|
|
385
|
+
echo "📝 Updating .replit configuration..."
|
|
386
|
+
if [ -f .replit ]; then
|
|
387
|
+
# Check if onBoot already exists
|
|
388
|
+
if ! grep -q "setup-claude-code.sh" .replit; then
|
|
389
|
+
echo "" >> .replit
|
|
390
|
+
echo '# Claude persistence (added by installer)' >> .replit
|
|
391
|
+
echo 'onBoot = "source /home/runner/workspace/scripts/setup-claude-code.sh 2>/dev/null || true"' >> .replit
|
|
392
|
+
fi
|
|
393
|
+
else
|
|
394
|
+
cat > .replit << 'REPLIT_EOF'
|
|
395
|
+
# Claude persistence
|
|
396
|
+
onBoot = "source /home/runner/workspace/scripts/setup-claude-code.sh 2>/dev/null || true"
|
|
397
|
+
REPLIT_EOF
|
|
398
|
+
fi
|
|
399
|
+
|
|
400
|
+
echo ""
|
|
401
|
+
echo -e "${GREEN}✅ Installation complete!${NC}"
|
|
402
|
+
echo ""
|
|
403
|
+
echo "What happens now:"
|
|
404
|
+
echo " • New shells will show the Claude session picker"
|
|
405
|
+
echo " • Your conversations persist across container restarts"
|
|
406
|
+
echo " • Claude binary is cached (faster startup)"
|
|
407
|
+
echo " • Bash history is preserved"
|
|
408
|
+
echo ""
|
|
409
|
+
echo "To test, open a new shell or run:"
|
|
410
|
+
echo " source ~/.config/bashrc"
|
|
411
|
+
echo ""
|
|
412
|
+
echo "Options:"
|
|
413
|
+
echo " Press 'c' - Continue last session"
|
|
414
|
+
echo " Press 'r' - Pick from session list"
|
|
415
|
+
echo " Press 'n' - New session"
|
|
416
|
+
echo " Press 's' - Skip (just a shell)"
|
|
417
|
+
echo ""
|
|
418
|
+
echo "To disable the menu: export CLAUDE_NO_PROMPT=true"
|
|
419
|
+
echo ""
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "replit-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Persist Claude Code sessions, auth, and history across Replit container restarts",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"replit-tools": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"replit",
|
|
14
|
+
"claude",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"persistence",
|
|
17
|
+
"session",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"author": "stevemoraco",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/stevemoraco/DATAtools"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/stevemoraco/DATAtools#readme",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=16.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# Claude Session Manager - Interactive Multi-Terminal Support
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Prompts user to choose: resume a session, start new, or skip.
|
|
6
|
+
# Supports multiple terminals with independent session tracking.
|
|
7
|
+
# =============================================================================
|
|
8
|
+
|
|
9
|
+
WORKSPACE="/home/runner/workspace"
|
|
10
|
+
SESSIONS_DIR="${WORKSPACE}/.claude-sessions"
|
|
11
|
+
LOCK_DIR="/tmp/.claude-locks"
|
|
12
|
+
|
|
13
|
+
mkdir -p "${SESSIONS_DIR}" "${LOCK_DIR}" 2>/dev/null
|
|
14
|
+
|
|
15
|
+
# Get terminal identifier
|
|
16
|
+
get_terminal_id() {
|
|
17
|
+
local tty_name=$(tty 2>/dev/null | sed 's|/dev/||' | tr '/' '-')
|
|
18
|
+
if [ -n "$tty_name" ] && [ "$tty_name" != "not" ]; then
|
|
19
|
+
echo "$tty_name"
|
|
20
|
+
else
|
|
21
|
+
echo "shell-$$"
|
|
22
|
+
fi
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
TERMINAL_ID=$(get_terminal_id)
|
|
26
|
+
STATE_FILE="${SESSIONS_DIR}/${TERMINAL_ID}.json"
|
|
27
|
+
|
|
28
|
+
# Get recent sessions with full details
|
|
29
|
+
get_recent_sessions() {
|
|
30
|
+
local history="${HOME}/.claude/history.jsonl"
|
|
31
|
+
local projects_dir="${HOME}/.claude/projects/-home-runner-workspace"
|
|
32
|
+
|
|
33
|
+
if [ -f "${history}" ]; then
|
|
34
|
+
# Collect all session data with full metadata
|
|
35
|
+
node -e "
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
const path = require('path');
|
|
38
|
+
const readline = require('readline');
|
|
39
|
+
|
|
40
|
+
const historyFile = '${history}';
|
|
41
|
+
const projectsDir = '${projects_dir}';
|
|
42
|
+
|
|
43
|
+
const sessionData = new Map();
|
|
44
|
+
|
|
45
|
+
// Read history to get session metadata
|
|
46
|
+
const lines = fs.readFileSync(historyFile, 'utf8').trim().split('\n');
|
|
47
|
+
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
try {
|
|
50
|
+
const j = JSON.parse(line);
|
|
51
|
+
if (!j.sessionId) continue;
|
|
52
|
+
|
|
53
|
+
if (!sessionData.has(j.sessionId)) {
|
|
54
|
+
sessionData.set(j.sessionId, {
|
|
55
|
+
id: j.sessionId,
|
|
56
|
+
firstSeen: j.timestamp,
|
|
57
|
+
lastSeen: j.timestamp,
|
|
58
|
+
firstPrompt: j.display || '',
|
|
59
|
+
lastPrompt: j.display || '',
|
|
60
|
+
messageCount: 0,
|
|
61
|
+
project: j.project || ''
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = sessionData.get(j.sessionId);
|
|
66
|
+
if (j.timestamp < data.firstSeen) {
|
|
67
|
+
data.firstSeen = j.timestamp;
|
|
68
|
+
data.firstPrompt = j.display || data.firstPrompt;
|
|
69
|
+
}
|
|
70
|
+
if (j.timestamp > data.lastSeen) {
|
|
71
|
+
data.lastSeen = j.timestamp;
|
|
72
|
+
data.lastPrompt = j.display || data.lastPrompt;
|
|
73
|
+
}
|
|
74
|
+
} catch(e) {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Enrich with .jsonl file data (message counts, file sizes)
|
|
78
|
+
for (const [id, data] of sessionData) {
|
|
79
|
+
const jsonlPath = path.join(projectsDir, id + '.jsonl');
|
|
80
|
+
const agentPath = path.join(projectsDir, 'agent-' + id.substring(0,7) + '.jsonl');
|
|
81
|
+
|
|
82
|
+
let filePath = null;
|
|
83
|
+
let fileSize = 0;
|
|
84
|
+
|
|
85
|
+
if (fs.existsSync(jsonlPath)) {
|
|
86
|
+
filePath = jsonlPath;
|
|
87
|
+
} else if (fs.existsSync(agentPath)) {
|
|
88
|
+
filePath = agentPath;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (filePath) {
|
|
92
|
+
try {
|
|
93
|
+
const stat = fs.statSync(filePath);
|
|
94
|
+
fileSize = stat.size;
|
|
95
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
96
|
+
const msgLines = content.trim().split('\n').filter(l => l.trim());
|
|
97
|
+
data.messageCount = msgLines.length;
|
|
98
|
+
data.fileSize = fileSize;
|
|
99
|
+
data.filePath = filePath;
|
|
100
|
+
} catch(e) {}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Sort by lastSeen descending and output
|
|
105
|
+
const sorted = Array.from(sessionData.values())
|
|
106
|
+
.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0))
|
|
107
|
+
.slice(0, 10);
|
|
108
|
+
|
|
109
|
+
sorted.forEach((s, i) => {
|
|
110
|
+
const formatTime = (ts) => {
|
|
111
|
+
if (!ts) return 'unknown';
|
|
112
|
+
const d = new Date(ts);
|
|
113
|
+
const utc = d.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
|
114
|
+
// MST is UTC-7
|
|
115
|
+
const mst = new Date(ts - 7*60*60*1000).toISOString().replace('T', ' ').substring(0, 19) + ' MST';
|
|
116
|
+
return utc + ' / ' + mst;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const timeAgo = (ts) => {
|
|
120
|
+
if (!ts) return '';
|
|
121
|
+
const mins = Math.round((Date.now() - ts) / 1000 / 60);
|
|
122
|
+
if (mins < 60) return mins + 'm ago';
|
|
123
|
+
if (mins < 1440) return Math.round(mins/60) + 'h ago';
|
|
124
|
+
return Math.round(mins/1440) + 'd ago';
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const sizeStr = (bytes) => {
|
|
128
|
+
if (!bytes) return '0B';
|
|
129
|
+
if (bytes < 1024) return bytes + 'B';
|
|
130
|
+
if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + 'KB';
|
|
131
|
+
return (bytes/1024/1024).toFixed(1) + 'MB';
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
console.log('SESSION|' + (i+1));
|
|
135
|
+
console.log('ID|' + s.id);
|
|
136
|
+
console.log('MESSAGES|' + (s.messageCount || '?'));
|
|
137
|
+
console.log('SIZE|' + sizeStr(s.fileSize));
|
|
138
|
+
console.log('LAST_ACTIVE|' + timeAgo(s.lastSeen));
|
|
139
|
+
console.log('STARTED|' + formatTime(s.firstSeen));
|
|
140
|
+
console.log('LAST_SEEN|' + formatTime(s.lastSeen));
|
|
141
|
+
console.log('FIRST_PROMPT|' + (s.firstPrompt || '').substring(0, 80).replace(/\\n/g, ' ').trim());
|
|
142
|
+
console.log('LAST_PROMPT|' + (s.lastPrompt || '').substring(0, 80).replace(/\\n/g, ' ').trim());
|
|
143
|
+
console.log('---');
|
|
144
|
+
});
|
|
145
|
+
" 2>/dev/null
|
|
146
|
+
fi
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Display formatted session list
|
|
150
|
+
show_sessions() {
|
|
151
|
+
local data=$(get_recent_sessions)
|
|
152
|
+
if [ -z "$data" ]; then
|
|
153
|
+
echo " No sessions found."
|
|
154
|
+
return
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
local current_num=""
|
|
158
|
+
|
|
159
|
+
echo "$data" | while IFS='|' read -r key value; do
|
|
160
|
+
case "$key" in
|
|
161
|
+
SESSION)
|
|
162
|
+
current_num="$value"
|
|
163
|
+
echo ""
|
|
164
|
+
echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
165
|
+
echo " [$value]"
|
|
166
|
+
;;
|
|
167
|
+
ID)
|
|
168
|
+
echo " ID: $value"
|
|
169
|
+
;;
|
|
170
|
+
MESSAGES)
|
|
171
|
+
printf " Messages: %s" "$value"
|
|
172
|
+
;;
|
|
173
|
+
SIZE)
|
|
174
|
+
printf " | Size: %s\n" "$value"
|
|
175
|
+
;;
|
|
176
|
+
LAST_ACTIVE)
|
|
177
|
+
echo " Active: $value"
|
|
178
|
+
;;
|
|
179
|
+
STARTED)
|
|
180
|
+
echo " Started: $value"
|
|
181
|
+
;;
|
|
182
|
+
LAST_SEEN)
|
|
183
|
+
echo " Last: $value"
|
|
184
|
+
;;
|
|
185
|
+
FIRST_PROMPT)
|
|
186
|
+
if [ -n "$value" ]; then
|
|
187
|
+
echo " First: \"$value\""
|
|
188
|
+
fi
|
|
189
|
+
;;
|
|
190
|
+
LAST_PROMPT)
|
|
191
|
+
if [ -n "$value" ]; then
|
|
192
|
+
echo " Latest: \"$value\""
|
|
193
|
+
fi
|
|
194
|
+
;;
|
|
195
|
+
esac
|
|
196
|
+
done
|
|
197
|
+
echo ""
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Count running Claude instances
|
|
201
|
+
count_claude_instances() {
|
|
202
|
+
pgrep -x "claude" 2>/dev/null | wc -l
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Save session state
|
|
206
|
+
save_session_state() {
|
|
207
|
+
local session_id="$1"
|
|
208
|
+
local flags="${2:---dangerously-skip-permissions}"
|
|
209
|
+
mkdir -p "${SESSIONS_DIR}"
|
|
210
|
+
cat > "${STATE_FILE}" << EOF
|
|
211
|
+
{
|
|
212
|
+
"sessionId": "${session_id}",
|
|
213
|
+
"flags": "${flags}",
|
|
214
|
+
"terminalId": "${TERMINAL_ID}",
|
|
215
|
+
"timestamp": $(date +%s)
|
|
216
|
+
}
|
|
217
|
+
EOF
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Get last session for this terminal
|
|
221
|
+
get_terminal_last_session() {
|
|
222
|
+
if [ -f "${STATE_FILE}" ]; then
|
|
223
|
+
node -e "try{console.log(require('${STATE_FILE}').sessionId||'')}catch(e){}" 2>/dev/null
|
|
224
|
+
fi
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# Interactive session picker
|
|
228
|
+
claude_prompt() {
|
|
229
|
+
# Only in interactive shells
|
|
230
|
+
[[ $- != *i* ]] && return 0
|
|
231
|
+
|
|
232
|
+
# Check if claude exists
|
|
233
|
+
if ! command -v claude &>/dev/null; then
|
|
234
|
+
return 0
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
local running=$(count_claude_instances)
|
|
238
|
+
local last_session=$(get_terminal_last_session)
|
|
239
|
+
|
|
240
|
+
echo ""
|
|
241
|
+
echo "╭─────────────────────────────────────────────────────────╮"
|
|
242
|
+
echo "│ Claude Session Manager │"
|
|
243
|
+
echo "╰─────────────────────────────────────────────────────────╯"
|
|
244
|
+
|
|
245
|
+
if [ "$running" -gt 0 ]; then
|
|
246
|
+
echo " ($running Claude instance(s) running in other terminals)"
|
|
247
|
+
fi
|
|
248
|
+
echo ""
|
|
249
|
+
|
|
250
|
+
# Show options
|
|
251
|
+
echo " [c] Continue last session for this terminal"
|
|
252
|
+
if [ -n "$last_session" ]; then
|
|
253
|
+
echo " └─ ${last_session:0:8}..."
|
|
254
|
+
fi
|
|
255
|
+
echo " [r] Resume a specific session (pick from list)"
|
|
256
|
+
echo " [n] Start new session"
|
|
257
|
+
echo " [s] Skip - just give me a shell"
|
|
258
|
+
echo ""
|
|
259
|
+
|
|
260
|
+
# Read choice with timeout
|
|
261
|
+
local choice
|
|
262
|
+
read -t 30 -n 1 -p " Choice [c/r/n/s]: " choice
|
|
263
|
+
echo ""
|
|
264
|
+
|
|
265
|
+
case "$choice" in
|
|
266
|
+
c|C|"")
|
|
267
|
+
# Continue last session (default on Enter or timeout)
|
|
268
|
+
if [ -n "$last_session" ]; then
|
|
269
|
+
echo ""
|
|
270
|
+
echo " Resuming session ${last_session:0:8}..."
|
|
271
|
+
claude -r "$last_session" --dangerously-skip-permissions
|
|
272
|
+
save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)"
|
|
273
|
+
else
|
|
274
|
+
echo ""
|
|
275
|
+
echo " No previous session for this terminal, starting new..."
|
|
276
|
+
claude --dangerously-skip-permissions
|
|
277
|
+
save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)"
|
|
278
|
+
fi
|
|
279
|
+
;;
|
|
280
|
+
r|R)
|
|
281
|
+
# Show session list with full details
|
|
282
|
+
echo ""
|
|
283
|
+
echo " Recent Sessions"
|
|
284
|
+
show_sessions
|
|
285
|
+
|
|
286
|
+
# Get session IDs for selection
|
|
287
|
+
local session_ids=$(get_recent_sessions | grep "^ID|" | cut -d'|' -f2)
|
|
288
|
+
|
|
289
|
+
read -p " Enter number (or 'q' to cancel): " session_num
|
|
290
|
+
|
|
291
|
+
if [ "$session_num" = "q" ] || [ -z "$session_num" ]; then
|
|
292
|
+
echo " Cancelled."
|
|
293
|
+
return 0
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
local selected_id=$(echo "$session_ids" | sed -n "${session_num}p")
|
|
297
|
+
if [ -n "$selected_id" ]; then
|
|
298
|
+
echo ""
|
|
299
|
+
echo " Resuming session: $selected_id"
|
|
300
|
+
claude -r "$selected_id" --dangerously-skip-permissions
|
|
301
|
+
save_session_state "$selected_id"
|
|
302
|
+
else
|
|
303
|
+
echo " Invalid selection."
|
|
304
|
+
fi
|
|
305
|
+
;;
|
|
306
|
+
n|N)
|
|
307
|
+
# Start new session
|
|
308
|
+
echo ""
|
|
309
|
+
echo " Starting new Claude session..."
|
|
310
|
+
claude --dangerously-skip-permissions
|
|
311
|
+
save_session_state "$(tail -1 "${HOME}/.claude/history.jsonl" 2>/dev/null | grep -oP '"sessionId":"[^"]+"' | cut -d'"' -f4)"
|
|
312
|
+
;;
|
|
313
|
+
s|S)
|
|
314
|
+
# Skip - just shell
|
|
315
|
+
echo ""
|
|
316
|
+
echo " Okay, just a shell. Type 'claude' or 'cr' when you want Claude."
|
|
317
|
+
;;
|
|
318
|
+
*)
|
|
319
|
+
echo ""
|
|
320
|
+
echo " Unknown option. Type 'claude' to start manually."
|
|
321
|
+
;;
|
|
322
|
+
esac
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Aliases for manual use
|
|
326
|
+
alias cr='claude -c --dangerously-skip-permissions'
|
|
327
|
+
alias claude-resume='claude -c --dangerously-skip-permissions'
|
|
328
|
+
alias claude-pick='claude -r --dangerously-skip-permissions'
|
|
329
|
+
alias claude-new='claude --dangerously-skip-permissions'
|
|
330
|
+
|
|
331
|
+
# Export for manual use
|
|
332
|
+
export -f get_recent_sessions
|
|
333
|
+
export -f save_session_state
|
|
334
|
+
export TERMINAL_ID
|
|
335
|
+
|
|
336
|
+
# Show the interactive prompt by default.
|
|
337
|
+
# Press 's' to skip and just get a shell.
|
|
338
|
+
# Set CLAUDE_NO_PROMPT=true to disable entirely.
|
|
339
|
+
|
|
340
|
+
alias claude-menu='claude_prompt'
|
|
341
|
+
|
|
342
|
+
if [ "${CLAUDE_NO_PROMPT}" != "true" ]; then
|
|
343
|
+
claude_prompt
|
|
344
|
+
fi
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# Claude Code Setup Script for Replit
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# This script ensures Claude Code is properly set up after container restarts.
|
|
6
|
+
# It handles:
|
|
7
|
+
# 1. Symlink for conversation history persistence (~/.claude)
|
|
8
|
+
# 2. Symlink for Claude binary (~/.local/bin/claude)
|
|
9
|
+
# 3. Authentication persistence (credentials stored in workspace)
|
|
10
|
+
# 4. Auto-installation if Claude is missing
|
|
11
|
+
#
|
|
12
|
+
# Run this script on every container restart via .config/bashrc or .replit
|
|
13
|
+
# =============================================================================
|
|
14
|
+
|
|
15
|
+
set -e
|
|
16
|
+
|
|
17
|
+
# Configuration
|
|
18
|
+
WORKSPACE="/home/runner/workspace"
|
|
19
|
+
CLAUDE_PERSISTENT="${WORKSPACE}/.claude-persistent"
|
|
20
|
+
CLAUDE_LOCAL_SHARE="${WORKSPACE}/.local/share/claude"
|
|
21
|
+
CLAUDE_VERSIONS="${CLAUDE_LOCAL_SHARE}/versions"
|
|
22
|
+
|
|
23
|
+
# Target locations (ephemeral, need symlinks)
|
|
24
|
+
CLAUDE_SYMLINK="${HOME}/.claude"
|
|
25
|
+
LOCAL_BIN="${HOME}/.local/bin"
|
|
26
|
+
LOCAL_SHARE_CLAUDE="${HOME}/.local/share/claude"
|
|
27
|
+
|
|
28
|
+
# Logging helper
|
|
29
|
+
log() {
|
|
30
|
+
if [[ $- == *i* ]]; then
|
|
31
|
+
echo "$1"
|
|
32
|
+
fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# Step 1: Ensure persistent directories exist
|
|
37
|
+
# =============================================================================
|
|
38
|
+
mkdir -p "${CLAUDE_PERSISTENT}"
|
|
39
|
+
mkdir -p "${CLAUDE_VERSIONS}"
|
|
40
|
+
mkdir -p "${LOCAL_BIN}"
|
|
41
|
+
mkdir -p "${HOME}/.local/share"
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# Step 2: Create ~/.claude symlink for conversation history & credentials
|
|
45
|
+
# =============================================================================
|
|
46
|
+
if [ ! -L "${CLAUDE_SYMLINK}" ] || [ "$(readlink -f "${CLAUDE_SYMLINK}")" != "${CLAUDE_PERSISTENT}" ]; then
|
|
47
|
+
rm -rf "${CLAUDE_SYMLINK}" 2>/dev/null || true
|
|
48
|
+
ln -sf "${CLAUDE_PERSISTENT}" "${CLAUDE_SYMLINK}"
|
|
49
|
+
log "✅ Claude history symlink: ~/.claude -> ${CLAUDE_PERSISTENT}"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# =============================================================================
|
|
53
|
+
# Step 3: Create ~/.local/share/claude symlink for installed versions
|
|
54
|
+
# =============================================================================
|
|
55
|
+
if [ ! -L "${LOCAL_SHARE_CLAUDE}" ] || [ "$(readlink -f "${LOCAL_SHARE_CLAUDE}")" != "${CLAUDE_LOCAL_SHARE}" ]; then
|
|
56
|
+
rm -rf "${LOCAL_SHARE_CLAUDE}" 2>/dev/null || true
|
|
57
|
+
ln -sf "${CLAUDE_LOCAL_SHARE}" "${LOCAL_SHARE_CLAUDE}"
|
|
58
|
+
log "✅ Claude versions symlink: ~/.local/share/claude -> ${CLAUDE_LOCAL_SHARE}"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# =============================================================================
|
|
62
|
+
# Step 4: Find latest Claude version and create binary symlink
|
|
63
|
+
# =============================================================================
|
|
64
|
+
LATEST_VERSION=""
|
|
65
|
+
if [ -d "${CLAUDE_VERSIONS}" ]; then
|
|
66
|
+
LATEST_VERSION=$(ls -1 "${CLAUDE_VERSIONS}" 2>/dev/null | sort -V | tail -n1)
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if [ -n "${LATEST_VERSION}" ] && [ -f "${CLAUDE_VERSIONS}/${LATEST_VERSION}" ]; then
|
|
70
|
+
CLAUDE_BINARY="${CLAUDE_VERSIONS}/${LATEST_VERSION}"
|
|
71
|
+
|
|
72
|
+
# Create or update the binary symlink
|
|
73
|
+
if [ ! -L "${LOCAL_BIN}/claude" ] || [ "$(readlink -f "${LOCAL_BIN}/claude")" != "${CLAUDE_BINARY}" ]; then
|
|
74
|
+
rm -f "${LOCAL_BIN}/claude" 2>/dev/null || true
|
|
75
|
+
ln -sf "${CLAUDE_BINARY}" "${LOCAL_BIN}/claude"
|
|
76
|
+
log "✅ Claude binary symlink: ~/.local/bin/claude -> ${CLAUDE_BINARY}"
|
|
77
|
+
fi
|
|
78
|
+
else
|
|
79
|
+
# Claude not installed - install it
|
|
80
|
+
log "⚠️ Claude Code not found, installing..."
|
|
81
|
+
|
|
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
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
else
|
|
106
|
+
log "❌ npm not found, cannot install Claude Code"
|
|
107
|
+
fi
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# =============================================================================
|
|
111
|
+
# Step 5: Ensure PATH includes ~/.local/bin
|
|
112
|
+
# =============================================================================
|
|
113
|
+
if [[ ":$PATH:" != *":${LOCAL_BIN}:"* ]]; then
|
|
114
|
+
export PATH="${LOCAL_BIN}:$PATH"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# =============================================================================
|
|
118
|
+
# Step 6: Verify authentication
|
|
119
|
+
# =============================================================================
|
|
120
|
+
CREDENTIALS_FILE="${CLAUDE_PERSISTENT}/.credentials.json"
|
|
121
|
+
if [ -f "${CREDENTIALS_FILE}" ]; then
|
|
122
|
+
# Check if credentials are valid (not expired)
|
|
123
|
+
if command -v node &> /dev/null; then
|
|
124
|
+
AUTH_INFO=$(node -e "
|
|
125
|
+
try {
|
|
126
|
+
const creds = require('${CREDENTIALS_FILE}');
|
|
127
|
+
const oauth = creds.claudeAiOauth;
|
|
128
|
+
const apiKey = creds.primaryApiKey;
|
|
129
|
+
if (apiKey) {
|
|
130
|
+
// Long-lived API key - doesn't expire
|
|
131
|
+
console.log('apikey');
|
|
132
|
+
} else if (oauth && oauth.expiresAt) {
|
|
133
|
+
console.log(oauth.expiresAt);
|
|
134
|
+
}
|
|
135
|
+
} catch(e) {}
|
|
136
|
+
" 2>/dev/null)
|
|
137
|
+
|
|
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"
|
|
147
|
+
else
|
|
148
|
+
log "✅ Claude authentication: valid (${HOURS_LEFT}h remaining)"
|
|
149
|
+
fi
|
|
150
|
+
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"
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
else
|
|
156
|
+
log "✅ Claude credentials file exists (persisted in workspace)"
|
|
157
|
+
fi
|
|
158
|
+
else
|
|
159
|
+
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"
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# =============================================================================
|
|
164
|
+
# Summary (only in interactive shells)
|
|
165
|
+
# =============================================================================
|
|
166
|
+
if [[ $- == *i* ]] && [ -n "${LATEST_VERSION}" ]; then
|
|
167
|
+
if command -v claude &> /dev/null; then
|
|
168
|
+
CLAUDE_VERSION=$(claude --version 2>/dev/null | head -1)
|
|
169
|
+
log "✅ Claude Code ready: ${CLAUDE_VERSION}"
|
|
170
|
+
fi
|
|
171
|
+
fi
|