claude-code-remote-pilot 0.2.13 → 0.3.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/CHANGELOG.md +26 -0
- package/README.md +19 -16
- package/bin/claude-pilot.js +212 -94
- package/lib/SessionManager.js +3 -0
- package/lib/config.js +17 -1
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 — 2026-05-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Session history**: every spawned or adopted session is persisted to `~/.claude-remote-pilot.json`. Sessions you've worked in before are remembered even after they end.
|
|
7
|
+
- **Offline sessions in watch**: the watch screen now shows offline sessions (from history but no longer running in tmux) alongside live sessions, in dim text.
|
|
8
|
+
- **Interactive watch**: press a number key (1–9) to select a session, then:
|
|
9
|
+
- Active session: `[t]` open terminal (tmux attach), `[k]` kill, `Esc` deselect
|
|
10
|
+
- Offline session: `[s]` re-spawn Claude at the saved path, `[r]` remove from history, `Esc` deselect
|
|
11
|
+
- **Auto-watch on start**: watch mode opens automatically after startup if there are any sessions (active or historical), so you land in the dashboard instead of a blank prompt.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Watch exits cleanly with `q` back to the command prompt (no session list required first).
|
|
15
|
+
- Watch command also triggers when there are only offline sessions in history.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 0.2.13 — previous
|
|
20
|
+
|
|
21
|
+
- Usage limit detection with auto-resume
|
|
22
|
+
- Token usage display in watch (`↑sent ↓received`)
|
|
23
|
+
- Configurable resume message
|
|
24
|
+
- Telegram notification persistence
|
|
25
|
+
- Session recovery after pilot restart
|
|
26
|
+
- Status detection: running / idle / needs-response / limit
|
package/README.md
CHANGED
|
@@ -15,29 +15,32 @@ npx claude-code-remote-pilot
|
|
|
15
15
|
│
|
|
16
16
|
├── asks: mount current directory as a session?
|
|
17
17
|
├── asks: set up Telegram? (optional)
|
|
18
|
-
└──
|
|
19
|
-
|
|
20
|
-
claude-pilot> spawn ~/projects/api-refactor
|
|
21
|
-
claude-pilot> spawn ~/projects/mobile-app
|
|
22
|
-
claude-pilot> watch
|
|
18
|
+
└── opens watch dashboard automatically
|
|
23
19
|
```
|
|
24
20
|
|
|
21
|
+
Watch opens immediately. Press `q` to drop to the command prompt, then `watch` to return.
|
|
22
|
+
|
|
25
23
|
```
|
|
26
24
|
Claude Code Remote Pilot
|
|
27
|
-
|
|
28
|
-
SESSION
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
───────────────────────────────────────────────────────────────────
|
|
26
|
+
# SESSION STATUS UP USAGE / RESET
|
|
27
|
+
───────────────────────────────────────────────────────────────────
|
|
28
|
+
1 api-refactor running 12m ↑1.2k ↓890
|
|
29
|
+
2 mobile-app limit 3m 1h 4m resets 2:00 AM
|
|
30
|
+
3 old-project offline —
|
|
31
|
+
───────────────────────────────────────────────────────────────────
|
|
32
|
+
[1-3]: select session q: exit watch
|
|
34
33
|
```
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
Press a number to select a session:
|
|
36
|
+
- **Active**: `[t]` open terminal · `[k]` kill · `Esc` back
|
|
37
|
+
- **Offline**: `[s]` re-spawn · `[r]` remove from history · `Esc` back
|
|
38
|
+
|
|
39
|
+
From any terminal, attach directly:
|
|
37
40
|
|
|
38
41
|
```bash
|
|
39
42
|
tmux attach -t api-refactor
|
|
40
|
-
# Ctrl+B then D to detach
|
|
43
|
+
# Ctrl+B then D to detach
|
|
41
44
|
```
|
|
42
45
|
|
|
43
46
|
---
|
|
@@ -100,7 +103,7 @@ sudo apt update && sudo apt install tmux
|
|
|
100
103
|
|---|---|
|
|
101
104
|
| `spawn <path> [name]` | Start Claude at a path. Name defaults to the directory name. |
|
|
102
105
|
| `list` | One-shot status of all sessions. |
|
|
103
|
-
| `watch` | Live
|
|
106
|
+
| `watch` | Live dashboard with offline session history. Press a number to select, `q` to exit. |
|
|
104
107
|
| `attach <name>` | Open a tmux session in the current terminal. |
|
|
105
108
|
| `kill <name>` | Stop a session. |
|
|
106
109
|
| `help` | Show command reference. |
|
|
@@ -171,7 +174,7 @@ Start Claude without `--dangerously-skip-permissions` unless you know what you'r
|
|
|
171
174
|
- [x] interactive REPL — spawn, watch, attach, kill
|
|
172
175
|
- [x] multi-session support
|
|
173
176
|
- [ ] web dashboard (sessions connect to pilot server)
|
|
174
|
-
- [
|
|
177
|
+
- [x] persistent session history with offline session display
|
|
175
178
|
- [ ] pluggable notification providers
|
|
176
179
|
- [ ] safety / policy engine
|
|
177
180
|
|
package/bin/claude-pilot.js
CHANGED
|
@@ -103,6 +103,8 @@ function formatStatus(session) {
|
|
|
103
103
|
const label = `limit ${Math.ceil(secs / 60)}m`;
|
|
104
104
|
return { plain: label, colored: `${C.yellow}${label}${C.reset}` };
|
|
105
105
|
}
|
|
106
|
+
case 'offline':
|
|
107
|
+
return { plain: 'offline', colored: `${C.dim}offline${C.reset}` };
|
|
106
108
|
case 'ended':
|
|
107
109
|
return { plain: 'ended', colored: `${C.dim}ended${C.reset}` };
|
|
108
110
|
default:
|
|
@@ -131,46 +133,161 @@ function formatUsage(session) {
|
|
|
131
133
|
return '';
|
|
132
134
|
}
|
|
133
135
|
|
|
134
|
-
|
|
136
|
+
// ─── watch mode ───────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function buildAllSessions(manager) {
|
|
139
|
+
const active = manager.list();
|
|
140
|
+
const activeNames = new Set(active.map(s => s.name));
|
|
141
|
+
const history = config.getHistory();
|
|
142
|
+
const offline = history
|
|
143
|
+
.filter(h => !activeNames.has(h.name))
|
|
144
|
+
.map(h => ({ name: h.name, path: h.path, status: 'offline', startedAt: h.lastSeen, resumeAt: null }));
|
|
145
|
+
return [...active, ...offline];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderWatchTable(allSessions, selectedIdx) {
|
|
135
149
|
const NW = 18, SW = 14, UW = 7, TW = 16;
|
|
136
150
|
const bar = ' ' + '─'.repeat(NW + SW + UW + TW + 10);
|
|
137
|
-
const header = ` ${'SESSION'.padEnd(NW)} ${'STATUS'.padEnd(SW)} ${'UP'.padEnd(UW)} ${'USAGE / RESET'.padEnd(TW)}`;
|
|
138
|
-
|
|
151
|
+
const header = ` ${'#'.padEnd(3)}${'SESSION'.padEnd(NW)} ${'STATUS'.padEnd(SW)} ${'UP'.padEnd(UW)} ${'USAGE / RESET'.padEnd(TW)}`;
|
|
152
|
+
|
|
153
|
+
const rows = allSessions.slice(0, 9).map((s, i) => {
|
|
154
|
+
const num = `${i + 1}`;
|
|
139
155
|
const { plain, colored } = formatStatus(s);
|
|
140
156
|
const pad = ' '.repeat(Math.max(0, SW - plain.length));
|
|
141
|
-
const usage = formatUsage(s);
|
|
142
|
-
|
|
157
|
+
const usage = s.status === 'offline' ? '' : formatUsage(s);
|
|
158
|
+
const up = s.status === 'offline' ? '—' : uptime(s.startedAt);
|
|
159
|
+
const sel = selectedIdx === i ? '▶' : ' ';
|
|
160
|
+
return ` ${sel}${num.padEnd(2)}${trunc(s.name, NW)} ${colored}${pad} ${up.padEnd(UW)} ${trunc(usage, TW)}`;
|
|
143
161
|
});
|
|
144
|
-
|
|
162
|
+
|
|
163
|
+
let footer;
|
|
164
|
+
if (selectedIdx >= 0 && selectedIdx < allSessions.length) {
|
|
165
|
+
const sel = allSessions[selectedIdx];
|
|
166
|
+
if (sel.status === 'offline') {
|
|
167
|
+
footer = ` [s] spawn [r] remove from history Esc: back`;
|
|
168
|
+
} else {
|
|
169
|
+
footer = ` [t] terminal [k] kill Esc: back`;
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
footer = ` [1-${Math.min(allSessions.length, 9)}]: select session q: exit watch`;
|
|
173
|
+
}
|
|
174
|
+
|
|
145
175
|
return ['\n', ' Claude Code Remote Pilot', bar, header, bar, ...rows, bar, footer, ''].join('\n');
|
|
146
176
|
}
|
|
147
177
|
|
|
148
|
-
// ─── watch mode ───────────────────────────────────────────────────────────────
|
|
149
|
-
|
|
150
178
|
function startWatch(manager, rl) {
|
|
179
|
+
let selectedIdx = -1;
|
|
180
|
+
let allSessions = buildAllSessions(manager);
|
|
181
|
+
const timer = { id: null };
|
|
182
|
+
|
|
151
183
|
function draw() {
|
|
184
|
+
allSessions = buildAllSessions(manager);
|
|
152
185
|
process.stdout.write('\x1B[2J\x1B[0f');
|
|
153
|
-
process.stdout.write(
|
|
186
|
+
process.stdout.write(renderWatchTable(allSessions, selectedIdx));
|
|
154
187
|
}
|
|
155
188
|
|
|
156
|
-
|
|
157
|
-
|
|
189
|
+
function startTimer() {
|
|
190
|
+
timer.id = setInterval(draw, 2000);
|
|
191
|
+
}
|
|
158
192
|
|
|
159
|
-
function
|
|
193
|
+
function stopTimer() {
|
|
194
|
+
clearInterval(timer.id);
|
|
195
|
+
timer.id = null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function exitWatch() {
|
|
160
199
|
process.stdin.removeListener('keypress', onKeypress);
|
|
161
|
-
|
|
200
|
+
stopTimer();
|
|
162
201
|
process.stdout.write('\x1B[2J\x1B[0f');
|
|
163
|
-
// Clear any character readline buffered while we were in watch mode
|
|
164
202
|
rl.write(null, { ctrl: true, name: 'u' });
|
|
165
203
|
rl.prompt();
|
|
166
204
|
}
|
|
167
205
|
|
|
206
|
+
function redraw() {
|
|
207
|
+
process.stdout.write('\x1B[2J\x1B[0f');
|
|
208
|
+
process.stdout.write(renderWatchTable(allSessions, selectedIdx));
|
|
209
|
+
}
|
|
210
|
+
|
|
168
211
|
function onKeypress(str, key) {
|
|
169
|
-
if (
|
|
170
|
-
|
|
212
|
+
if (!key) return;
|
|
213
|
+
|
|
214
|
+
if (key.ctrl && key.name === 'c') { exitWatch(); return; }
|
|
215
|
+
|
|
216
|
+
if (key.name === 'escape') {
|
|
217
|
+
selectedIdx = -1;
|
|
218
|
+
redraw();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (selectedIdx < 0) {
|
|
223
|
+
if (str === 'q' || str === 'Q') { exitWatch(); return; }
|
|
224
|
+
const n = parseInt(str);
|
|
225
|
+
if (!isNaN(n) && n >= 1 && n <= Math.min(allSessions.length, 9)) {
|
|
226
|
+
selectedIdx = n - 1;
|
|
227
|
+
redraw();
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const sel = allSessions[selectedIdx];
|
|
233
|
+
|
|
234
|
+
if (sel.status === 'offline') {
|
|
235
|
+
if (str === 's' || str === 'S') {
|
|
236
|
+
selectedIdx = -1;
|
|
237
|
+
stopTimer();
|
|
238
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
239
|
+
process.stdout.write('\x1B[2J\x1B[0f');
|
|
240
|
+
try {
|
|
241
|
+
const session = manager.spawn(sel.path, sel.name);
|
|
242
|
+
process.stdout.write(`\n ✓ "${session.name}" spawned.\n tmux attach -t ${session.name}\n\n`);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
process.stdout.write(`\n Error: ${e.message}\n\n`);
|
|
245
|
+
}
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
allSessions = buildAllSessions(manager);
|
|
248
|
+
draw();
|
|
249
|
+
startTimer();
|
|
250
|
+
process.stdin.on('keypress', onKeypress);
|
|
251
|
+
}, 1500);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (str === 'r' || str === 'R') {
|
|
255
|
+
config.removeFromHistory(sel.name);
|
|
256
|
+
selectedIdx = -1;
|
|
257
|
+
allSessions = buildAllSessions(manager);
|
|
258
|
+
redraw();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
if (str === 't' || str === 'T') {
|
|
263
|
+
stopTimer();
|
|
264
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
265
|
+
rl.pause();
|
|
266
|
+
process.stdout.write('\x1B[2J\x1B[0f');
|
|
267
|
+
const child = spawn('tmux', ['attach-session', '-t', sel.name], { stdio: 'inherit' });
|
|
268
|
+
child.on('exit', () => {
|
|
269
|
+
process.stdout.write('\n');
|
|
270
|
+
rl.resume();
|
|
271
|
+
selectedIdx = -1;
|
|
272
|
+
allSessions = buildAllSessions(manager);
|
|
273
|
+
draw();
|
|
274
|
+
startTimer();
|
|
275
|
+
process.stdin.on('keypress', onKeypress);
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (str === 'k' || str === 'K') {
|
|
280
|
+
try { manager.kill(sel.name); } catch {}
|
|
281
|
+
selectedIdx = -1;
|
|
282
|
+
allSessions = buildAllSessions(manager);
|
|
283
|
+
redraw();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
171
286
|
}
|
|
172
287
|
}
|
|
173
288
|
|
|
289
|
+
draw();
|
|
290
|
+
startTimer();
|
|
174
291
|
process.stdin.on('keypress', onKeypress);
|
|
175
292
|
}
|
|
176
293
|
|
|
@@ -208,20 +325,82 @@ const HELP = `
|
|
|
208
325
|
exit Quit pilot (asks whether to kill sessions)
|
|
209
326
|
`;
|
|
210
327
|
|
|
211
|
-
|
|
212
|
-
|
|
328
|
+
// ─── main ─────────────────────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
async function main() {
|
|
331
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
332
|
+
console.log(`
|
|
333
|
+
Claude Code Remote Pilot
|
|
334
|
+
|
|
335
|
+
Usage:
|
|
336
|
+
claude-remote-pilot
|
|
337
|
+
|
|
338
|
+
Interactive commands:
|
|
339
|
+
${HELP}`);
|
|
340
|
+
process.exit(0);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const setupRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
344
|
+
|
|
345
|
+
await ensureDep(setupRl, 'tmux', 'tmux', tmuxInstallCmd());
|
|
346
|
+
await ensureDep(setupRl, 'claude', 'Claude Code CLI', 'npm install -g @anthropic-ai/claude-code');
|
|
347
|
+
const telegram = await setupTelegram(setupRl);
|
|
348
|
+
|
|
349
|
+
const cfg = config.load();
|
|
350
|
+
const manager = new SessionManager({ telegram, resumeCommand: cfg.resumeCommand });
|
|
351
|
+
|
|
352
|
+
// Recover sessions from previous run
|
|
353
|
+
const savedSessions = (cfg.sessions || []).filter(s => {
|
|
354
|
+
try { execSync(`tmux has-session -t "${s.name}"`, { stdio: 'ignore' }); return true; }
|
|
355
|
+
catch { return false; }
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if (savedSessions.length) {
|
|
359
|
+
console.log(`\n Found ${savedSessions.length} session(s) still running from last time:`);
|
|
360
|
+
savedSessions.forEach(s => console.log(` ${s.name.padEnd(22)} ${s.path}`));
|
|
361
|
+
const recover = await question(setupRl, ' Re-adopt and watch them? (Y/n) ');
|
|
362
|
+
if (isYes(recover)) {
|
|
363
|
+
savedSessions.forEach(s => {
|
|
364
|
+
try { manager.adopt(s.name, s.path); console.log(` ✓ Re-adopted "${s.name}"`); }
|
|
365
|
+
catch (e) { console.log(` ✗ Could not adopt "${s.name}": ${e.message}`); }
|
|
366
|
+
});
|
|
367
|
+
console.log('');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const cwd = process.cwd();
|
|
372
|
+
const defaultName = path.basename(cwd);
|
|
373
|
+
const mount = await question(setupRl, `Mount current directory as a session? (${defaultName}) [Y/n] `);
|
|
374
|
+
|
|
375
|
+
if (isYes(mount)) {
|
|
376
|
+
const rawName = await questionRaw(setupRl, `Session name [${defaultName}]: `);
|
|
377
|
+
const session = manager.spawn(cwd, rawName || defaultName);
|
|
378
|
+
console.log(` ✓ "${session.name}" started at ${session.path}`);
|
|
379
|
+
console.log(` tmux attach -t ${session.name}\n`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
setupRl.close();
|
|
383
|
+
|
|
384
|
+
console.log(' Type help for commands.\n');
|
|
385
|
+
const replRl = readline.createInterface({
|
|
213
386
|
input: process.stdin,
|
|
214
387
|
output: process.stdout,
|
|
215
388
|
prompt: 'claude-pilot> ',
|
|
216
389
|
});
|
|
217
390
|
|
|
218
|
-
|
|
391
|
+
// Auto-enter watch if there are sessions to monitor
|
|
392
|
+
const allAtStart = buildAllSessions(manager);
|
|
393
|
+
if (allAtStart.length) {
|
|
394
|
+
startWatch(manager, replRl);
|
|
395
|
+
} else {
|
|
396
|
+
replRl.prompt();
|
|
397
|
+
}
|
|
219
398
|
|
|
220
|
-
|
|
399
|
+
replRl.on('line', async (line) => {
|
|
221
400
|
const parts = line.trim().split(/\s+/).filter(Boolean);
|
|
222
401
|
const [cmd, ...args] = parts;
|
|
223
402
|
|
|
224
|
-
if (!cmd) {
|
|
403
|
+
if (!cmd) { replRl.prompt(); return; }
|
|
225
404
|
|
|
226
405
|
try {
|
|
227
406
|
switch (cmd) {
|
|
@@ -244,16 +423,16 @@ function startREPL(manager) {
|
|
|
244
423
|
break;
|
|
245
424
|
}
|
|
246
425
|
case 'watch': {
|
|
247
|
-
const
|
|
248
|
-
if (!
|
|
249
|
-
startWatch(manager,
|
|
426
|
+
const all = buildAllSessions(manager);
|
|
427
|
+
if (!all.length) { console.log(' No sessions.'); break; }
|
|
428
|
+
startWatch(manager, replRl);
|
|
250
429
|
return;
|
|
251
430
|
}
|
|
252
431
|
case 'attach': {
|
|
253
432
|
if (!args[0]) { console.log(' Usage: attach <name>'); break; }
|
|
254
|
-
|
|
433
|
+
replRl.pause();
|
|
255
434
|
const child = spawn('tmux', ['attach-session', '-t', args[0]], { stdio: 'inherit' });
|
|
256
|
-
child.on('exit', () => { process.stdout.write('\n');
|
|
435
|
+
child.on('exit', () => { process.stdout.write('\n'); replRl.resume(); replRl.prompt(); });
|
|
257
436
|
return;
|
|
258
437
|
}
|
|
259
438
|
case 'kill': {
|
|
@@ -264,10 +443,10 @@ function startREPL(manager) {
|
|
|
264
443
|
}
|
|
265
444
|
case 'resume': {
|
|
266
445
|
if (args.length) {
|
|
267
|
-
const
|
|
268
|
-
manager.resumeCommand =
|
|
269
|
-
config.saveResumeCommand(
|
|
270
|
-
console.log(` ✓ Resume message saved: "${
|
|
446
|
+
const resumeMsg = args.join(' ');
|
|
447
|
+
manager.resumeCommand = resumeMsg;
|
|
448
|
+
config.saveResumeCommand(resumeMsg);
|
|
449
|
+
console.log(` ✓ Resume message saved: "${resumeMsg}"`);
|
|
271
450
|
} else {
|
|
272
451
|
console.log(` Current resume message: "${manager.resumeCommand || '(default)'}"`);
|
|
273
452
|
}
|
|
@@ -279,7 +458,7 @@ function startREPL(manager) {
|
|
|
279
458
|
}
|
|
280
459
|
case 'exit':
|
|
281
460
|
case 'quit': {
|
|
282
|
-
await handleExit(manager,
|
|
461
|
+
await handleExit(manager, replRl);
|
|
283
462
|
return;
|
|
284
463
|
}
|
|
285
464
|
default:
|
|
@@ -289,74 +468,13 @@ function startREPL(manager) {
|
|
|
289
468
|
console.error(` Error: ${err.message}`);
|
|
290
469
|
}
|
|
291
470
|
|
|
292
|
-
|
|
471
|
+
replRl.prompt();
|
|
293
472
|
});
|
|
294
473
|
|
|
295
|
-
|
|
296
|
-
// stdin closed (Ctrl+D) — can't ask interactively, keep sessions running
|
|
474
|
+
replRl.on('close', () => {
|
|
297
475
|
console.log('\n Sessions keep running. Use tmux to attach.\n');
|
|
298
476
|
process.exit(0);
|
|
299
477
|
});
|
|
300
478
|
}
|
|
301
479
|
|
|
302
|
-
// ─── main ─────────────────────────────────────────────────────────────────────
|
|
303
|
-
|
|
304
|
-
async function main() {
|
|
305
|
-
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
306
|
-
console.log(`
|
|
307
|
-
Claude Code Remote Pilot
|
|
308
|
-
|
|
309
|
-
Usage:
|
|
310
|
-
claude-remote-pilot
|
|
311
|
-
|
|
312
|
-
Interactive commands:
|
|
313
|
-
${HELP}`);
|
|
314
|
-
process.exit(0);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const setupRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
318
|
-
|
|
319
|
-
await ensureDep(setupRl, 'tmux', 'tmux', tmuxInstallCmd());
|
|
320
|
-
await ensureDep(setupRl, 'claude', 'Claude Code CLI', 'npm install -g @anthropic-ai/claude-code');
|
|
321
|
-
const telegram = await setupTelegram(setupRl);
|
|
322
|
-
|
|
323
|
-
const cfg = config.load();
|
|
324
|
-
const manager = new SessionManager({ telegram, resumeCommand: cfg.resumeCommand });
|
|
325
|
-
|
|
326
|
-
// Recover sessions from previous run
|
|
327
|
-
const savedSessions = (cfg.sessions || []).filter(s => {
|
|
328
|
-
try { execSync(`tmux has-session -t "${s.name}"`, { stdio: 'ignore' }); return true; }
|
|
329
|
-
catch { return false; }
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
if (savedSessions.length) {
|
|
333
|
-
console.log(`\n Found ${savedSessions.length} session(s) still running from last time:`);
|
|
334
|
-
savedSessions.forEach(s => console.log(` ${s.name.padEnd(22)} ${s.path}`));
|
|
335
|
-
const recover = await question(setupRl, ' Re-adopt and watch them? (Y/n) ');
|
|
336
|
-
if (isYes(recover)) {
|
|
337
|
-
savedSessions.forEach(s => {
|
|
338
|
-
try { manager.adopt(s.name, s.path); console.log(` ✓ Re-adopted "${s.name}"`); }
|
|
339
|
-
catch (e) { console.log(` ✗ Could not adopt "${s.name}": ${e.message}`); }
|
|
340
|
-
});
|
|
341
|
-
console.log('');
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const cwd = process.cwd();
|
|
346
|
-
const defaultName = path.basename(cwd);
|
|
347
|
-
const mount = await question(setupRl, `Mount current directory as a session? (${defaultName}) [Y/n] `);
|
|
348
|
-
|
|
349
|
-
if (isYes(mount)) {
|
|
350
|
-
const rawName = await questionRaw(setupRl, `Session name [${defaultName}]: `);
|
|
351
|
-
const session = manager.spawn(cwd, rawName || defaultName);
|
|
352
|
-
console.log(` ✓ "${session.name}" started at ${session.path}`);
|
|
353
|
-
console.log(` tmux attach -t ${session.name}\n`);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
setupRl.close();
|
|
357
|
-
|
|
358
|
-
console.log(' Type help for commands.\n');
|
|
359
|
-
startREPL(manager);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
480
|
main().catch(err => { console.error(err.message); process.exit(1); });
|
package/lib/SessionManager.js
CHANGED
|
@@ -3,6 +3,7 @@ const { execSync } = require('child_process');
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const Watcher = require('./Watcher');
|
|
6
|
+
const config = require('./config');
|
|
6
7
|
|
|
7
8
|
const RESERVED = new Set(['spawn', 'list', 'watch', 'attach', 'kill', 'help', 'exit', 'quit', 'resume']);
|
|
8
9
|
|
|
@@ -44,6 +45,7 @@ class SessionManager {
|
|
|
44
45
|
const watcher = this._makeWatcher(session);
|
|
45
46
|
watcher.start();
|
|
46
47
|
this.sessions.set(sessionName, { session, watcher });
|
|
48
|
+
config.addToHistory(sessionName, resolved);
|
|
47
49
|
return session;
|
|
48
50
|
}
|
|
49
51
|
|
|
@@ -57,6 +59,7 @@ class SessionManager {
|
|
|
57
59
|
const watcher = this._makeWatcher(session);
|
|
58
60
|
watcher.start();
|
|
59
61
|
this.sessions.set(name, { session, watcher });
|
|
62
|
+
config.addToHistory(name, dirPath);
|
|
60
63
|
return session;
|
|
61
64
|
}
|
|
62
65
|
|
package/lib/config.js
CHANGED
|
@@ -34,4 +34,20 @@ function saveResumeCommand(cmd) {
|
|
|
34
34
|
save({ resumeCommand: cmd });
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function addToHistory(name, path) {
|
|
38
|
+
const cfg = load();
|
|
39
|
+
const history = (cfg.sessionHistory || []).filter(s => s.name !== name);
|
|
40
|
+
history.unshift({ name, path, lastSeen: new Date().toISOString() });
|
|
41
|
+
save({ sessionHistory: history.slice(0, 30) }); // cap at 30
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function removeFromHistory(name) {
|
|
45
|
+
const cfg = load();
|
|
46
|
+
save({ sessionHistory: (cfg.sessionHistory || []).filter(s => s.name !== name) });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getHistory() {
|
|
50
|
+
return load().sessionHistory || [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { load, saveTelegram, saveSessions, clearSessions, saveResumeCommand, addToHistory, removeFromHistory, getHistory };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-remote-pilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Interactive Claude Code supervisor — spawn and monitor multiple Claude sessions from a single terminal.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"bin/",
|
|
11
11
|
"lib/",
|
|
12
12
|
"README.md",
|
|
13
|
+
"CHANGELOG.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|