claude-code-remote-pilot 0.5.1 → 0.5.2
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/CHANGELOG.md +7 -0
- package/README.md +153 -6
- package/lib/SessionManager.js +10 -4
- package/lib/Watcher.js +35 -3
- package/lib/WebServer.js +14 -2
- package/lib/config.js +2 -2
- package/lib/ui.html +40 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.2 — 2026-05-06
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Auto-resume now waits for reset time**: when Claude shows an explicit `resets at HH:MM` clock time, `Watcher` now resumes at that exact reset timestamp (including next-day rollover) instead of relying only on relative wait parsing.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
3
10
|
## 0.5.1 — 2026-05-06
|
|
4
11
|
|
|
5
12
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,10 +1,65 @@
|
|
|
1
1
|
# Claude Code Remote Pilot
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Keep Claude Code running while you're away from your desk.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A small self-hosted helper tool for people who run long Claude Code sessions and are tired of manually resuming after token limits.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Originally built just for personal use… then it slowly grew 😅
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why?
|
|
14
|
+
|
|
15
|
+
I kept hitting the same problem:
|
|
16
|
+
|
|
17
|
+
- Claude Code stopped after hitting token limits
|
|
18
|
+
- long-running tasks needed babysitting
|
|
19
|
+
- sometimes I wanted to leave home while a task was still running
|
|
20
|
+
- checking progress remotely was annoying
|
|
21
|
+
|
|
22
|
+
At first I only wanted auto-resume. Then it slowly turned into a small remote workflow tool.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## What this is NOT
|
|
27
|
+
|
|
28
|
+
- a hosted platform
|
|
29
|
+
- an "AI agent framework"
|
|
30
|
+
- a replacement for Claude Code
|
|
31
|
+
- a polished enterprise product
|
|
32
|
+
|
|
33
|
+
It's just a practical helper tool for people running Claude Code for long periods.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- Auto-detect Claude Code limit states and resume automatically
|
|
40
|
+
- Persistent tmux-based sessions that outlive the pilot process
|
|
41
|
+
- Web UI for monitoring and control from phone or any browser — with full ANSI color terminal rendering
|
|
42
|
+
- Telegram notifications when sessions need attention
|
|
43
|
+
- Browser desktop notifications on status changes
|
|
44
|
+
- Broadcast a message to all active sessions at once
|
|
45
|
+
- Lightweight, self-hosted — just Node.js and tmux
|
|
46
|
+
- Experimental but surprisingly useful 👀
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Current Status
|
|
51
|
+
|
|
52
|
+
Very experimental. Built quickly to scratch a personal itch, so expect rough edges. If you try it and hit weird issues, feel free to open an issue or PR.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Typical Workflow
|
|
57
|
+
|
|
58
|
+
1. Start the pilot and spawn Claude Code sessions
|
|
59
|
+
2. Leave it running — go touch grass
|
|
60
|
+
3. Pilot detects limit hits and auto-resumes
|
|
61
|
+
4. Get notified via Telegram or browser notification
|
|
62
|
+
5. Check progress remotely from your phone
|
|
8
63
|
|
|
9
64
|
---
|
|
10
65
|
|
|
@@ -142,6 +197,91 @@ You can also bind to a specific interface IP: `web 3742 192.168.1.10`.
|
|
|
142
197
|
|
|
143
198
|
---
|
|
144
199
|
|
|
200
|
+
## Remote access via Cloudflare Tunnel (recommended)
|
|
201
|
+
|
|
202
|
+
Binding to `0.0.0.0` exposes the dashboard on your local network but not the internet. For secure remote access from anywhere — phone, another machine, a coffee shop — use a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/).
|
|
203
|
+
|
|
204
|
+
This is the recommended setup for remote work: the dashboard stays on `127.0.0.1` (never directly exposed), and Cloudflare handles TLS, authentication, and routing.
|
|
205
|
+
|
|
206
|
+
### Quick start (no domain required)
|
|
207
|
+
|
|
208
|
+
Install `cloudflared`:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# macOS
|
|
212
|
+
brew install cloudflare/cloudflare/cloudflared
|
|
213
|
+
|
|
214
|
+
# Linux
|
|
215
|
+
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
|
|
216
|
+
chmod +x cloudflared && sudo mv cloudflared /usr/local/bin/
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Start the pilot's web dashboard, then in a second terminal run:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
cloudflared tunnel --url http://127.0.0.1:3742
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Cloudflare prints a random `https://*.trycloudflare.com` URL. Open it on any device. The tunnel closes when you stop `cloudflared`.
|
|
226
|
+
|
|
227
|
+
### Persistent tunnel with a custom domain
|
|
228
|
+
|
|
229
|
+
If you have a domain on Cloudflare, you can get a stable URL and add Cloudflare Access (zero-trust auth) in front of the dashboard.
|
|
230
|
+
|
|
231
|
+
**1. Authenticate and create a tunnel:**
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
cloudflared tunnel login
|
|
235
|
+
cloudflared tunnel create claude-pilot
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**2. Create `~/.cloudflared/config.yml`:**
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
tunnel: <your-tunnel-id>
|
|
242
|
+
credentials-file: /home/<user>/.cloudflared/<tunnel-id>.json
|
|
243
|
+
|
|
244
|
+
ingress:
|
|
245
|
+
- hostname: pilot.yourdomain.com
|
|
246
|
+
service: http://127.0.0.1:3742
|
|
247
|
+
- service: http_status:404
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**3. Add a DNS record:**
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
cloudflared tunnel route dns claude-pilot pilot.yourdomain.com
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**4. Start the tunnel:**
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
cloudflared tunnel run claude-pilot
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The dashboard is now reachable at `https://pilot.yourdomain.com`.
|
|
263
|
+
|
|
264
|
+
### Adding authentication (Cloudflare Access)
|
|
265
|
+
|
|
266
|
+
Cloudflare Access puts a login wall in front of the tunnel — no inbound ports, no VPN.
|
|
267
|
+
|
|
268
|
+
1. Go to **Cloudflare Zero Trust → Access → Applications → Add an application**
|
|
269
|
+
2. Choose **Self-hosted**, set the domain to `pilot.yourdomain.com`
|
|
270
|
+
3. Add a policy: allow your email address (or Google/GitHub OAuth)
|
|
271
|
+
|
|
272
|
+
After this, anyone reaching `pilot.yourdomain.com` must authenticate with Cloudflare before the dashboard loads.
|
|
273
|
+
|
|
274
|
+
### Run the tunnel as a background service
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
# Install as a system service (runs on boot)
|
|
278
|
+
sudo cloudflared service install
|
|
279
|
+
sudo systemctl start cloudflared # Linux
|
|
280
|
+
sudo launchctl start cloudflared # macOS
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
145
285
|
## Telegram setup
|
|
146
286
|
|
|
147
287
|
Create a bot via `@BotFather` and get your token.
|
|
@@ -211,12 +351,19 @@ Start Claude without `--dangerously-skip-permissions` unless you know what you'r
|
|
|
211
351
|
- [x] broadcast message to all sessions
|
|
212
352
|
- [x] auto-discover untracked tmux sessions on startup
|
|
213
353
|
- [ ] auto-yes rules — confirm prompts automatically by pattern
|
|
354
|
+
- [ ] smarter retry logic
|
|
355
|
+
- [ ] usage statistics and session timeline
|
|
356
|
+
- [ ] remote command queue
|
|
214
357
|
- [ ] pluggable notification providers
|
|
215
358
|
|
|
216
359
|
---
|
|
217
360
|
|
|
218
|
-
##
|
|
361
|
+
## Contributing
|
|
362
|
+
|
|
363
|
+
PRs, ideas, and weird experiments are welcome 😄
|
|
364
|
+
|
|
365
|
+
---
|
|
219
366
|
|
|
220
|
-
|
|
367
|
+
## License
|
|
221
368
|
|
|
222
|
-
|
|
369
|
+
MIT
|
package/lib/SessionManager.js
CHANGED
|
@@ -26,7 +26,7 @@ class SessionManager {
|
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
spawn(dirPath, name) {
|
|
29
|
+
spawn(dirPath, name, command = 'claude') {
|
|
30
30
|
const resolved = path.resolve(dirPath.replace(/^~/, process.env.HOME || ''));
|
|
31
31
|
if (!fs.existsSync(resolved)) throw new Error(`Path not found: ${resolved}`);
|
|
32
32
|
|
|
@@ -39,16 +39,22 @@ class SessionManager {
|
|
|
39
39
|
execSync(`tmux kill-session -t "${sessionName}"`, { stdio: 'ignore' });
|
|
40
40
|
} catch {}
|
|
41
41
|
|
|
42
|
-
execSync(`tmux new-session -d -s "${sessionName}" -c "${resolved}" "
|
|
42
|
+
execSync(`tmux new-session -d -s "${sessionName}" -c "${resolved}" "${command}"`, { stdio: 'ignore' });
|
|
43
43
|
|
|
44
|
-
const session = { name: sessionName, path: resolved, status: 'running', startedAt: new Date(), resumeAt: null };
|
|
44
|
+
const session = { name: sessionName, path: resolved, command, status: 'running', startedAt: new Date(), resumeAt: null };
|
|
45
45
|
const watcher = this._makeWatcher(session);
|
|
46
46
|
watcher.start();
|
|
47
47
|
this.sessions.set(sessionName, { session, watcher });
|
|
48
|
-
config.addToHistory(sessionName, resolved);
|
|
48
|
+
config.addToHistory(sessionName, resolved, command);
|
|
49
49
|
return session;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
respawn(name) {
|
|
53
|
+
const h = config.getHistory().find(e => e.name === name);
|
|
54
|
+
if (!h) throw new Error(`No history for session "${name}"`);
|
|
55
|
+
return this.spawn(h.path, name, h.command || 'claude');
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
adopt(name, dirPath) {
|
|
53
59
|
try { execSync(`tmux has-session -t "${name}"`, { stdio: 'ignore' }); }
|
|
54
60
|
catch { throw new Error(`tmux session "${name}" not found.`); }
|
package/lib/Watcher.js
CHANGED
|
@@ -80,6 +80,35 @@ class Watcher {
|
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
_parseResetAtMs(text) {
|
|
84
|
+
const atMatch = text.match(RESET_AT_RE);
|
|
85
|
+
if (atMatch) {
|
|
86
|
+
const raw = atMatch[1].trim().toLowerCase();
|
|
87
|
+
const m = raw.match(/^(\d{1,2}):(\d{2})\s*(am|pm)?$/);
|
|
88
|
+
if (m) {
|
|
89
|
+
let hour = parseInt(m[1], 10);
|
|
90
|
+
const minute = parseInt(m[2], 10);
|
|
91
|
+
const suffix = m[3];
|
|
92
|
+
if (suffix === 'pm' && hour !== 12) hour += 12;
|
|
93
|
+
if (suffix === 'am' && hour === 12) hour = 0;
|
|
94
|
+
|
|
95
|
+
const now = new Date();
|
|
96
|
+
const resetAt = new Date(now);
|
|
97
|
+
resetAt.setHours(hour, minute, 0, 0);
|
|
98
|
+
if (resetAt.getTime() <= now.getTime()) {
|
|
99
|
+
resetAt.setDate(resetAt.getDate() + 1);
|
|
100
|
+
}
|
|
101
|
+
return resetAt.getTime();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const inMatch = text.match(/(?:try again|retry|wait).*?in\s+(\d+)\s*(second|minute|hour)/i);
|
|
106
|
+
if (!inMatch) return null;
|
|
107
|
+
const v = parseInt(inMatch[1], 10);
|
|
108
|
+
const mult = inMatch[2].startsWith('second') ? 1 : inMatch[2].startsWith('minute') ? 60 : 3600;
|
|
109
|
+
return Date.now() + (v * mult * 1000);
|
|
110
|
+
}
|
|
111
|
+
|
|
83
112
|
async _check() {
|
|
84
113
|
if (this._busy) return;
|
|
85
114
|
this._busy = true;
|
|
@@ -137,16 +166,19 @@ class Watcher {
|
|
|
137
166
|
|
|
138
167
|
this.lastHash = hash;
|
|
139
168
|
const wait = this._parseWait(text);
|
|
169
|
+
const resetAtMs = this._parseResetAtMs(text);
|
|
140
170
|
const resetTime = this._parseResetTime(text);
|
|
171
|
+
const effectiveResumeAtMs = resetAtMs || (Date.now() + wait * 1000);
|
|
172
|
+
const effectiveWaitSeconds = Math.max(1, Math.ceil((effectiveResumeAtMs - Date.now()) / 1000));
|
|
141
173
|
|
|
142
174
|
this.session.status = 'limit';
|
|
143
|
-
this.session.resumeAt =
|
|
175
|
+
this.session.resumeAt = effectiveResumeAtMs;
|
|
144
176
|
this.session.resetTime = resetTime;
|
|
145
177
|
|
|
146
178
|
notifier.send(this.telegram.token, this.telegram.chatId,
|
|
147
|
-
`Pilot: limit in "${this.session.name}". Resets ${resetTime || `in ${Math.ceil(
|
|
179
|
+
`Pilot: limit in "${this.session.name}". Resets ${resetTime || `in ${Math.ceil(effectiveWaitSeconds / 60)}m`}.`);
|
|
148
180
|
|
|
149
|
-
await new Promise(r => setTimeout(r,
|
|
181
|
+
await new Promise(r => setTimeout(r, effectiveWaitSeconds * 1000));
|
|
150
182
|
|
|
151
183
|
try { spawnSync('tmux', ['send-keys', '-t', this.session.name, this.resumeCommand, 'Enter'], { stdio: 'ignore' }); }
|
|
152
184
|
catch {}
|
package/lib/WebServer.js
CHANGED
|
@@ -93,9 +93,9 @@ class WebServer {
|
|
|
93
93
|
if (req.method === 'POST' && pathname === '/api/sessions') {
|
|
94
94
|
return this._readBody(req, (err, body) => {
|
|
95
95
|
if (err) return this._json(res, 400, { error: err.message });
|
|
96
|
-
const { name, path: dirPath, prompt: initialPrompt } = body;
|
|
96
|
+
const { name, path: dirPath, prompt: initialPrompt, command } = body;
|
|
97
97
|
try {
|
|
98
|
-
const session = this.manager.spawn(dirPath, name);
|
|
98
|
+
const session = this.manager.spawn(dirPath, name, command || 'claude');
|
|
99
99
|
if (initialPrompt) {
|
|
100
100
|
setTimeout(() => {
|
|
101
101
|
spawnSync('tmux', ['send-keys', '-t', session.name, initialPrompt, 'Enter']);
|
|
@@ -108,6 +108,18 @@ class WebServer {
|
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// POST /api/sessions/:name/respawn
|
|
112
|
+
const respawnMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/respawn$/);
|
|
113
|
+
if (req.method === 'POST' && respawnMatch) {
|
|
114
|
+
const name = decodeURIComponent(respawnMatch[1]);
|
|
115
|
+
try {
|
|
116
|
+
const session = this.manager.respawn(name);
|
|
117
|
+
return this._json(res, 200, { ...session, id: session.name });
|
|
118
|
+
} catch (e) {
|
|
119
|
+
return this._json(res, 400, { error: e.message });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
111
123
|
// GET /api/sessions/:name/output
|
|
112
124
|
const outputMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/output$/);
|
|
113
125
|
if (req.method === 'GET' && outputMatch) {
|
package/lib/config.js
CHANGED
|
@@ -34,10 +34,10 @@ function saveResumeCommand(cmd) {
|
|
|
34
34
|
save({ resumeCommand: cmd });
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function addToHistory(name, path) {
|
|
37
|
+
function addToHistory(name, path, command = 'claude') {
|
|
38
38
|
const cfg = load();
|
|
39
39
|
const history = (cfg.sessionHistory || []).filter(s => s.name !== name);
|
|
40
|
-
history.unshift({ name, path, lastSeen: new Date().toISOString() });
|
|
40
|
+
history.unshift({ name, path, command, lastSeen: new Date().toISOString() });
|
|
41
41
|
save({ sessionHistory: history.slice(0, 30) }); // cap at 30
|
|
42
42
|
}
|
|
43
43
|
|
package/lib/ui.html
CHANGED
|
@@ -660,6 +660,18 @@ function SessionDetailScreen({ session, onBack, onKilled }) {
|
|
|
660
660
|
}
|
|
661
661
|
};
|
|
662
662
|
|
|
663
|
+
const handleRespawn = async () => {
|
|
664
|
+
try {
|
|
665
|
+
const res = await apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/respawn`, { method: 'POST' });
|
|
666
|
+
if (!res.ok) {
|
|
667
|
+
const d = await res.json();
|
|
668
|
+
alert(d.error || 'Failed to respawn');
|
|
669
|
+
}
|
|
670
|
+
} catch (e) {
|
|
671
|
+
if (e.message !== 'Unauthorized') alert('Network error');
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
|
|
663
675
|
// Render terminal HTML (ANSI → HTML, with plain-text fallback)
|
|
664
676
|
let _termHtml = '';
|
|
665
677
|
if (output) {
|
|
@@ -687,6 +699,11 @@ function SessionDetailScreen({ session, onBack, onKilled }) {
|
|
|
687
699
|
<div className="detail-meta" style={{ maxWidth: 480, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{session.path}</div>
|
|
688
700
|
</div>
|
|
689
701
|
<div className="detail-actions">
|
|
702
|
+
{isOffline && (
|
|
703
|
+
<button className="btn btn-sm btn-primary" onClick={handleRespawn}>
|
|
704
|
+
↺ Respawn
|
|
705
|
+
</button>
|
|
706
|
+
)}
|
|
690
707
|
{!isOffline && (
|
|
691
708
|
<button className="btn btn-sm" onClick={copyAttachCmd} title={`tmux attach -t ${session.name}`}>
|
|
692
709
|
{copyOk ? '✓ Copied' : '⊞ New Terminal'}
|
|
@@ -752,6 +769,7 @@ function SessionDetailScreen({ session, onBack, onKilled }) {
|
|
|
752
769
|
<div className="card">
|
|
753
770
|
<h3>Session Info</h3>
|
|
754
771
|
<div className="info-row"><span className="info-label">Status</span><StatusPill status={session.status} /></div>
|
|
772
|
+
<div className="info-row"><span className="info-label">Agent</span><span className="info-value" style={{ fontSize: 11 }}>{session.command || 'claude'}</span></div>
|
|
755
773
|
<div className="info-row"><span className="info-label">Started</span><span className="info-value" style={{ fontSize: 11 }}>{relativeTime(session.startedAt)}</span></div>
|
|
756
774
|
<div className="info-row"><span className="info-label">Tokens</span><span className="info-value" style={{ fontSize: 11 }}>{formatTokens(session.tokens)}</span></div>
|
|
757
775
|
{session.status === 'limit' && session.resumeAt && (
|
|
@@ -770,18 +788,23 @@ function CreateSessionScreen({ onBack, onCreated }) {
|
|
|
770
788
|
const [name, setName] = useState('');
|
|
771
789
|
const [path, setPath] = useState('');
|
|
772
790
|
const [prompt, setPrompt] = useState('');
|
|
791
|
+
const [command, setCommand] = useState('claude');
|
|
792
|
+
const [customCommand, setCustomCommand] = useState('');
|
|
773
793
|
const [error, setError] = useState('');
|
|
774
794
|
const [loading, setLoading] = useState(false);
|
|
775
795
|
|
|
796
|
+
const resolvedCommand = command === 'custom' ? customCommand.trim() : command;
|
|
797
|
+
|
|
776
798
|
const handleCreate = async () => {
|
|
777
799
|
if (!name.trim() || !path.trim()) { setError('Session name and working directory are required.'); return; }
|
|
800
|
+
if (command === 'custom' && !customCommand.trim()) { setError('Enter a custom command.'); return; }
|
|
778
801
|
setError('');
|
|
779
802
|
setLoading(true);
|
|
780
803
|
try {
|
|
781
804
|
const res = await apiFetch('/api/sessions', {
|
|
782
805
|
method: 'POST',
|
|
783
806
|
headers: { 'Content-Type': 'application/json' },
|
|
784
|
-
body: JSON.stringify({ name: name.trim(), path: path.trim(), prompt: prompt.trim() || undefined }),
|
|
807
|
+
body: JSON.stringify({ name: name.trim(), path: path.trim(), prompt: prompt.trim() || undefined, command: resolvedCommand }),
|
|
785
808
|
});
|
|
786
809
|
const data = await res.json();
|
|
787
810
|
if (!res.ok) { setError(data.error || 'Failed to create session.'); return; }
|
|
@@ -807,10 +830,24 @@ function CreateSessionScreen({ onBack, onCreated }) {
|
|
|
807
830
|
<label className="form-label">Working directory</label>
|
|
808
831
|
<input className="form-input" placeholder="~/projects/app" value={path} onChange={e => setPath(e.target.value)} />
|
|
809
832
|
</div>
|
|
833
|
+
<div className="form-group">
|
|
834
|
+
<label className="form-label">Agent</label>
|
|
835
|
+
<select className="form-select" value={command} onChange={e => setCommand(e.target.value)}>
|
|
836
|
+
<option value="claude">claude (Claude Code)</option>
|
|
837
|
+
<option value="opencode">opencode</option>
|
|
838
|
+
<option value="custom">custom…</option>
|
|
839
|
+
</select>
|
|
840
|
+
{command === 'custom' && (
|
|
841
|
+
<input className="form-input" style={{ marginTop: 8 }} placeholder="e.g. aider" value={customCommand} onChange={e => setCustomCommand(e.target.value)} />
|
|
842
|
+
)}
|
|
843
|
+
{command === 'opencode' && (
|
|
844
|
+
<div className="form-hint">Status indicators (running/idle) are claude-only — opencode sessions will show as idle but are fully controllable.</div>
|
|
845
|
+
)}
|
|
846
|
+
</div>
|
|
810
847
|
<div className="form-group">
|
|
811
848
|
<label className="form-label">Initial prompt <span style={{ fontWeight: 400, color: 'var(--muted)' }}>(optional)</span></label>
|
|
812
|
-
<textarea className="form-textarea" placeholder="What should
|
|
813
|
-
<div className="form-hint">Sent
|
|
849
|
+
<textarea className="form-textarea" placeholder="What should the agent work on?" value={prompt} onChange={e => setPrompt(e.target.value)} rows={4} />
|
|
850
|
+
<div className="form-hint">Sent 2 seconds after the session starts.</div>
|
|
814
851
|
</div>
|
|
815
852
|
{error && <div style={{ marginBottom: 16, padding: '10px 14px', background: 'var(--error-soft)', color: 'var(--error)', borderRadius: 'var(--radius-sm)', fontSize: 13 }}>{error}</div>}
|
|
816
853
|
<div className="form-actions">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-remote-pilot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Interactive Claude Code supervisor — spawn and monitor multiple Claude sessions from a single terminal.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"repository": {
|
|
@@ -38,4 +38,4 @@
|
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=18"
|
|
40
40
|
}
|
|
41
|
-
}
|
|
41
|
+
}
|