claude-code-remote-pilot 0.2.8 → 0.2.10
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/bin/claude-pilot.js +26 -6
- package/lib/SessionManager.js +21 -20
- package/lib/Watcher.js +30 -9
- package/lib/config.js +5 -1
- package/package.json +1 -1
package/bin/claude-pilot.js
CHANGED
|
@@ -111,14 +111,21 @@ function trunc(str, len) {
|
|
|
111
111
|
return str.length <= len ? str.padEnd(len) : str.slice(0, len - 1) + '…';
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
function uptime(startedAt) {
|
|
115
|
+
const s = Math.floor((Date.now() - new Date(startedAt)) / 1000);
|
|
116
|
+
if (s < 60) return `${s}s`;
|
|
117
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
|
118
|
+
return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
|
|
119
|
+
}
|
|
120
|
+
|
|
114
121
|
function renderTable(sessions) {
|
|
115
|
-
const NW =
|
|
116
|
-
const bar = ' ' + '─'.repeat(NW + SW +
|
|
117
|
-
const header = ` ${'SESSION'.padEnd(NW)} ${'STATUS'.padEnd(SW)} DIRECTORY`;
|
|
122
|
+
const NW = 18, SW = 14, UW = 8;
|
|
123
|
+
const bar = ' ' + '─'.repeat(NW + SW + UW + 34);
|
|
124
|
+
const header = ` ${'SESSION'.padEnd(NW)} ${'STATUS'.padEnd(SW)} ${'UP'.padEnd(UW)} DIRECTORY`;
|
|
118
125
|
const rows = sessions.map(s => {
|
|
119
126
|
const { plain, colored } = formatStatus(s);
|
|
120
127
|
const pad = ' '.repeat(Math.max(0, SW - plain.length));
|
|
121
|
-
return ` ${trunc(s.name, NW)} ${colored}${pad} ${trunc(s.path,
|
|
128
|
+
return ` ${trunc(s.name, NW)} ${colored}${pad} ${uptime(s.startedAt).padEnd(UW)} ${trunc(s.path, 32)}`;
|
|
122
129
|
});
|
|
123
130
|
const footer = ` ${sessions.length} session${sessions.length !== 1 ? 's' : ''} ${new Date().toLocaleTimeString()} q to exit`;
|
|
124
131
|
return ['\n', ' Claude Code Remote Pilot', bar, header, bar, ...rows, bar, footer, ''].join('\n');
|
|
@@ -182,6 +189,7 @@ const HELP = `
|
|
|
182
189
|
watch Live session monitor (q to exit)
|
|
183
190
|
attach <name> Open tmux session in this terminal
|
|
184
191
|
kill <name> Stop a session
|
|
192
|
+
resume [message] Show or set the message sent after a limit resets
|
|
185
193
|
help Show this help
|
|
186
194
|
exit Quit pilot (asks whether to kill sessions)
|
|
187
195
|
`;
|
|
@@ -240,6 +248,17 @@ function startREPL(manager) {
|
|
|
240
248
|
console.log(` ✓ "${args[0]}" killed.`);
|
|
241
249
|
break;
|
|
242
250
|
}
|
|
251
|
+
case 'resume': {
|
|
252
|
+
if (args.length) {
|
|
253
|
+
const cmd = args.join(' ');
|
|
254
|
+
manager.resumeCommand = cmd;
|
|
255
|
+
config.saveResumeCommand(cmd);
|
|
256
|
+
console.log(` ✓ Resume message saved: "${cmd}"`);
|
|
257
|
+
} else {
|
|
258
|
+
console.log(` Current resume message: "${manager.resumeCommand || '(default)'}"`);
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
243
262
|
case 'help': {
|
|
244
263
|
console.log(HELP);
|
|
245
264
|
break;
|
|
@@ -287,10 +306,11 @@ ${HELP}`);
|
|
|
287
306
|
await ensureDep(setupRl, 'claude', 'Claude Code CLI', 'npm install -g @anthropic-ai/claude-code');
|
|
288
307
|
const telegram = await setupTelegram(setupRl);
|
|
289
308
|
|
|
290
|
-
const
|
|
309
|
+
const cfg = config.load();
|
|
310
|
+
const manager = new SessionManager({ telegram, resumeCommand: cfg.resumeCommand });
|
|
291
311
|
|
|
292
312
|
// Recover sessions from previous run
|
|
293
|
-
const savedSessions = (
|
|
313
|
+
const savedSessions = (cfg.sessions || []).filter(s => {
|
|
294
314
|
try { execSync(`tmux has-session -t "${s.name}"`, { stdio: 'ignore' }); return true; }
|
|
295
315
|
catch { return false; }
|
|
296
316
|
});
|
package/lib/SessionManager.js
CHANGED
|
@@ -4,16 +4,25 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const Watcher = require('./Watcher');
|
|
6
6
|
|
|
7
|
-
const RESERVED = new Set(['spawn', 'list', 'watch', 'attach', 'kill', 'help', 'exit', 'quit']);
|
|
7
|
+
const RESERVED = new Set(['spawn', 'list', 'watch', 'attach', 'kill', 'help', 'exit', 'quit', 'resume']);
|
|
8
8
|
|
|
9
9
|
function sanitizeName(name) {
|
|
10
10
|
return name.replace(/[.:\s]/g, '-');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
class SessionManager {
|
|
14
|
-
constructor({ telegram = {} } = {}) {
|
|
14
|
+
constructor({ telegram = {}, resumeCommand } = {}) {
|
|
15
15
|
this.sessions = new Map();
|
|
16
16
|
this.telegram = telegram;
|
|
17
|
+
this.resumeCommand = resumeCommand;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_makeWatcher(session) {
|
|
21
|
+
return new Watcher(session, {
|
|
22
|
+
telegram: this.telegram,
|
|
23
|
+
resumeCommand: this.resumeCommand,
|
|
24
|
+
onEnded: (s) => this.sessions.delete(s.name),
|
|
25
|
+
});
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
spawn(dirPath, name) {
|
|
@@ -32,25 +41,12 @@ class SessionManager {
|
|
|
32
41
|
execSync(`tmux new-session -d -s "${sessionName}" -c "${resolved}" "claude"`, { stdio: 'ignore' });
|
|
33
42
|
|
|
34
43
|
const session = { name: sessionName, path: resolved, status: 'running', startedAt: new Date(), resumeAt: null };
|
|
35
|
-
|
|
36
|
-
const watcher = new Watcher(session, {
|
|
37
|
-
telegram: this.telegram,
|
|
38
|
-
onEnded: (s) => this.sessions.delete(s.name),
|
|
39
|
-
});
|
|
44
|
+
const watcher = this._makeWatcher(session);
|
|
40
45
|
watcher.start();
|
|
41
|
-
|
|
42
46
|
this.sessions.set(sessionName, { session, watcher });
|
|
43
47
|
return session;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
kill(name) {
|
|
47
|
-
const entry = this.sessions.get(name);
|
|
48
|
-
if (!entry) throw new Error(`Session "${name}" not found.`);
|
|
49
|
-
entry.watcher.stop();
|
|
50
|
-
try { execSync(`tmux kill-session -t "${name}"`, { stdio: 'ignore' }); } catch {}
|
|
51
|
-
this.sessions.delete(name);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
50
|
adopt(name, dirPath) {
|
|
55
51
|
try { execSync(`tmux has-session -t "${name}"`, { stdio: 'ignore' }); }
|
|
56
52
|
catch { throw new Error(`tmux session "${name}" not found.`); }
|
|
@@ -58,15 +54,20 @@ class SessionManager {
|
|
|
58
54
|
if (this.sessions.has(name)) throw new Error(`Session "${name}" already being watched.`);
|
|
59
55
|
|
|
60
56
|
const session = { name, path: dirPath, status: 'running', startedAt: new Date(), resumeAt: null };
|
|
61
|
-
const watcher =
|
|
62
|
-
telegram: this.telegram,
|
|
63
|
-
onEnded: (s) => this.sessions.delete(s.name),
|
|
64
|
-
});
|
|
57
|
+
const watcher = this._makeWatcher(session);
|
|
65
58
|
watcher.start();
|
|
66
59
|
this.sessions.set(name, { session, watcher });
|
|
67
60
|
return session;
|
|
68
61
|
}
|
|
69
62
|
|
|
63
|
+
kill(name) {
|
|
64
|
+
const entry = this.sessions.get(name);
|
|
65
|
+
if (!entry) throw new Error(`Session "${name}" not found.`);
|
|
66
|
+
entry.watcher.stop();
|
|
67
|
+
try { execSync(`tmux kill-session -t "${name}"`, { stdio: 'ignore' }); } catch {}
|
|
68
|
+
this.sessions.delete(name);
|
|
69
|
+
}
|
|
70
|
+
|
|
70
71
|
killAll() {
|
|
71
72
|
for (const name of [...this.sessions.keys()]) {
|
|
72
73
|
try { this.kill(name); } catch {}
|
package/lib/Watcher.js
CHANGED
|
@@ -4,11 +4,11 @@ const crypto = require('crypto');
|
|
|
4
4
|
const notifier = require('./notifier');
|
|
5
5
|
|
|
6
6
|
const LIMIT_RE = /hit your limit|usage limit|rate limit|limit reached|try again|resets/i;
|
|
7
|
-
//
|
|
7
|
+
// Checked against full capture — these messages persist on screen
|
|
8
8
|
const RESPONSE_RE = /do you want to proceed|esc to cancel|ctrl\+e to explain|❯\s*\d+\.\s*yes/i;
|
|
9
|
-
//
|
|
9
|
+
// Checked against last 5 lines only — footer disappears when Claude finishes
|
|
10
10
|
const RUNNING_RE = /esc to interrupt/i;
|
|
11
|
-
//
|
|
11
|
+
// Checked against the single last non-empty line
|
|
12
12
|
const IDLE_RE = /^\s*>\s*$/;
|
|
13
13
|
|
|
14
14
|
class Watcher {
|
|
@@ -20,10 +20,17 @@ class Watcher {
|
|
|
20
20
|
this.fallbackWait = opts.fallbackWait || 300;
|
|
21
21
|
this.cooldown = opts.cooldown || 180;
|
|
22
22
|
this.captureLines = opts.captureLines || 500;
|
|
23
|
+
this.resumeCommand = opts.resumeCommand || 'The usage limit has reset. Please continue where you left off.';
|
|
23
24
|
this.lastHash = '';
|
|
24
25
|
this.lastResumeAt = 0;
|
|
25
26
|
this._timer = null;
|
|
26
27
|
this._busy = false;
|
|
28
|
+
this._prevOutputHash = '';
|
|
29
|
+
this._outputUnchangedCount = 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_stripAnsi(text) {
|
|
33
|
+
return text.replace(/\x1b\[[0-9;]*[mGKHFABCDJsuhl]/g, '').replace(/\x1b[()][AB012]/g, '');
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
start() {
|
|
@@ -72,23 +79,36 @@ class Watcher {
|
|
|
72
79
|
return;
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
const
|
|
76
|
-
const
|
|
82
|
+
const raw = this._capture();
|
|
83
|
+
const text = this._stripAnsi(raw);
|
|
84
|
+
const nonEmptyLines = text.split('\n').filter(l => l.trim());
|
|
85
|
+
const lastLine = nonEmptyLines[nonEmptyLines.length - 1] || '';
|
|
86
|
+
const recentLines = nonEmptyLines.slice(-5).join('\n');
|
|
87
|
+
|
|
88
|
+
// Track whether output is changing between checks
|
|
89
|
+
const outputHash = this._hash(recentLines);
|
|
90
|
+
if (outputHash === this._prevOutputHash) {
|
|
91
|
+
this._outputUnchangedCount++;
|
|
92
|
+
} else {
|
|
93
|
+
this._prevOutputHash = outputHash;
|
|
94
|
+
this._outputUnchangedCount = 0;
|
|
95
|
+
}
|
|
96
|
+
const outputIsStable = this._outputUnchangedCount >= 2;
|
|
77
97
|
|
|
78
98
|
if (LIMIT_RE.test(text)) {
|
|
79
99
|
await this._handleLimit(text);
|
|
80
|
-
} else if (RESPONSE_RE.test(
|
|
100
|
+
} else if (RESPONSE_RE.test(recentLines)) {
|
|
81
101
|
if (this.session.status !== 'needs-response') {
|
|
82
102
|
this.session.status = 'needs-response';
|
|
83
103
|
notifier.send(this.telegram.token, this.telegram.chatId,
|
|
84
104
|
`Pilot: "${this.session.name}" needs your response.`);
|
|
85
105
|
}
|
|
86
|
-
} else if (RUNNING_RE.test(
|
|
106
|
+
} else if (RUNNING_RE.test(recentLines)) {
|
|
87
107
|
if (this.session.status !== 'running') {
|
|
88
108
|
this.session.status = 'running';
|
|
89
109
|
this.session.resumeAt = null;
|
|
90
110
|
}
|
|
91
|
-
} else if (IDLE_RE.test(lastLine)) {
|
|
111
|
+
} else if (IDLE_RE.test(lastLine) || outputIsStable) {
|
|
92
112
|
if (this.session.status !== 'idle') {
|
|
93
113
|
this.session.status = 'idle';
|
|
94
114
|
this.session.resumeAt = null;
|
|
@@ -108,6 +128,7 @@ class Watcher {
|
|
|
108
128
|
if ((Date.now() / 1000) - this.lastResumeAt < this.cooldown) return;
|
|
109
129
|
|
|
110
130
|
this.lastHash = hash;
|
|
131
|
+
this._outputUnchangedCount = 0; // reset — we're intentionally waiting
|
|
111
132
|
const wait = this._parseWait(text);
|
|
112
133
|
this.session.status = 'limit';
|
|
113
134
|
this.session.resumeAt = Date.now() + wait * 1000;
|
|
@@ -117,7 +138,7 @@ class Watcher {
|
|
|
117
138
|
|
|
118
139
|
await new Promise(r => setTimeout(r, wait * 1000));
|
|
119
140
|
|
|
120
|
-
try { execSync(`tmux send-keys -t "${this.session.name}" "
|
|
141
|
+
try { execSync(`tmux send-keys -t "${this.session.name}" "${this.resumeCommand}" Enter`, { stdio: 'ignore' }); }
|
|
121
142
|
catch {}
|
|
122
143
|
|
|
123
144
|
this.lastResumeAt = Date.now() / 1000;
|
package/lib/config.js
CHANGED
|
@@ -30,4 +30,8 @@ function clearSessions() {
|
|
|
30
30
|
save({ sessions: [] });
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
function saveResumeCommand(cmd) {
|
|
34
|
+
save({ resumeCommand: cmd });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { load, saveTelegram, saveSessions, clearSessions, saveResumeCommand };
|
package/package.json
CHANGED