claude-code-remote-pilot 0.2.10 → 0.2.12
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 +28 -14
- package/lib/Watcher.js +32 -6
- package/package.json +1 -1
package/bin/claude-pilot.js
CHANGED
|
@@ -32,11 +32,14 @@ function tmuxInstallCmd() {
|
|
|
32
32
|
return 'sudo apt-get install -y tmux';
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function isYes(answer) { return answer === '' || answer === 'y' || answer === 'yes'; }
|
|
36
|
+
function isNo(answer) { return answer === '' || answer === 'n' || answer === 'no'; }
|
|
37
|
+
|
|
35
38
|
async function ensureDep(rl, cmd, label, installCmd) {
|
|
36
39
|
if (has(cmd)) return;
|
|
37
40
|
console.log(`\n${label} is not installed.`);
|
|
38
|
-
const answer = await question(rl, 'Install it now? (
|
|
39
|
-
if (answer
|
|
41
|
+
const answer = await question(rl, 'Install it now? (Y/n) ');
|
|
42
|
+
if (!isYes(answer)) {
|
|
40
43
|
console.log(`Run manually: ${installCmd}`);
|
|
41
44
|
process.exit(1);
|
|
42
45
|
}
|
|
@@ -67,8 +70,8 @@ async function setupTelegram(rl) {
|
|
|
67
70
|
return saved;
|
|
68
71
|
}
|
|
69
72
|
console.log('\nTelegram notifications (optional).');
|
|
70
|
-
const answer = await question(rl, 'Set up Telegram now? (y/
|
|
71
|
-
if (answer
|
|
73
|
+
const answer = await question(rl, 'Set up Telegram now? (y/N) ');
|
|
74
|
+
if (answer === '' || !isYes(answer)) { console.log('Skipping.\n'); return {}; }
|
|
72
75
|
const token = await questionRaw(rl, 'Bot token: ');
|
|
73
76
|
const chatId = await questionRaw(rl, 'Chat ID: ');
|
|
74
77
|
config.saveTelegram(token, chatId);
|
|
@@ -118,14 +121,25 @@ function uptime(startedAt) {
|
|
|
118
121
|
return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
function formatUsage(session) {
|
|
125
|
+
if (session.status === 'limit' && session.resetTime) {
|
|
126
|
+
return `resets ${session.resetTime}`;
|
|
127
|
+
}
|
|
128
|
+
if (session.tokens) {
|
|
129
|
+
return `↑${session.tokens.sent} ↓${session.tokens.received}`;
|
|
130
|
+
}
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
function renderTable(sessions) {
|
|
122
|
-
const NW = 18, SW = 14, UW =
|
|
123
|
-
const bar = ' ' + '─'.repeat(NW + SW + UW +
|
|
124
|
-
const header = ` ${'SESSION'.padEnd(NW)} ${'STATUS'.padEnd(SW)} ${'UP'.padEnd(UW)}
|
|
135
|
+
const NW = 18, SW = 14, UW = 7, TW = 16;
|
|
136
|
+
const bar = ' ' + '─'.repeat(NW + SW + UW + TW + 10);
|
|
137
|
+
const header = ` ${'SESSION'.padEnd(NW)} ${'STATUS'.padEnd(SW)} ${'UP'.padEnd(UW)} ${'USAGE / RESET'.padEnd(TW)}`;
|
|
125
138
|
const rows = sessions.map(s => {
|
|
126
139
|
const { plain, colored } = formatStatus(s);
|
|
127
140
|
const pad = ' '.repeat(Math.max(0, SW - plain.length));
|
|
128
|
-
|
|
141
|
+
const usage = formatUsage(s);
|
|
142
|
+
return ` ${trunc(s.name, NW)} ${colored}${pad} ${uptime(s.startedAt).padEnd(UW)} ${trunc(usage, TW)}`;
|
|
129
143
|
});
|
|
130
144
|
const footer = ` ${sessions.length} session${sessions.length !== 1 ? 's' : ''} ${new Date().toLocaleTimeString()} q to exit`;
|
|
131
145
|
return ['\n', ' Claude Code Remote Pilot', bar, header, bar, ...rows, bar, footer, ''].join('\n');
|
|
@@ -169,8 +183,8 @@ async function handleExit(manager, rl) {
|
|
|
169
183
|
console.log('');
|
|
170
184
|
process.exit(0);
|
|
171
185
|
}
|
|
172
|
-
const answer = await
|
|
173
|
-
if (answer
|
|
186
|
+
const answer = await question(rl, `\n Kill all ${sessions.length} session(s) before exiting? (y/N) `);
|
|
187
|
+
if (isYes(answer) && answer !== '') {
|
|
174
188
|
manager.killAll();
|
|
175
189
|
config.clearSessions();
|
|
176
190
|
console.log(' All sessions killed.\n');
|
|
@@ -318,8 +332,8 @@ ${HELP}`);
|
|
|
318
332
|
if (savedSessions.length) {
|
|
319
333
|
console.log(`\n Found ${savedSessions.length} session(s) still running from last time:`);
|
|
320
334
|
savedSessions.forEach(s => console.log(` ${s.name.padEnd(22)} ${s.path}`));
|
|
321
|
-
const recover = await question(setupRl, ' Re-adopt and watch them? (
|
|
322
|
-
if (recover
|
|
335
|
+
const recover = await question(setupRl, ' Re-adopt and watch them? (Y/n) ');
|
|
336
|
+
if (isYes(recover)) {
|
|
323
337
|
savedSessions.forEach(s => {
|
|
324
338
|
try { manager.adopt(s.name, s.path); console.log(` ✓ Re-adopted "${s.name}"`); }
|
|
325
339
|
catch (e) { console.log(` ✗ Could not adopt "${s.name}": ${e.message}`); }
|
|
@@ -330,9 +344,9 @@ ${HELP}`);
|
|
|
330
344
|
|
|
331
345
|
const cwd = process.cwd();
|
|
332
346
|
const defaultName = path.basename(cwd);
|
|
333
|
-
const mount = await question(setupRl, `Mount current directory as a session? (${defaultName}) [
|
|
347
|
+
const mount = await question(setupRl, `Mount current directory as a session? (${defaultName}) [Y/n] `);
|
|
334
348
|
|
|
335
|
-
if (mount
|
|
349
|
+
if (isYes(mount)) {
|
|
336
350
|
const rawName = await questionRaw(setupRl, `Session name [${defaultName}]: `);
|
|
337
351
|
const session = manager.spawn(cwd, rawName || defaultName);
|
|
338
352
|
console.log(` ✓ "${session.name}" started at ${session.path}`);
|
package/lib/Watcher.js
CHANGED
|
@@ -4,12 +4,13 @@ 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
|
-
// Checked against full capture — these messages persist on screen
|
|
8
7
|
const RESPONSE_RE = /do you want to proceed|esc to cancel|ctrl\+e to explain|❯\s*\d+\.\s*yes/i;
|
|
9
|
-
// Checked against last 5 lines only — footer disappears when Claude finishes
|
|
10
8
|
const RUNNING_RE = /esc to interrupt/i;
|
|
11
|
-
// Checked against the single last non-empty line
|
|
12
9
|
const IDLE_RE = /^\s*>\s*$/;
|
|
10
|
+
// Claude Code footer: "tokens: ↑1,234 ↓567" or "↑1.2k ↓890"
|
|
11
|
+
const TOKEN_RE = /↑\s*([\d.,]+[km]?)\s*↓\s*([\d.,]+[km]?)/i;
|
|
12
|
+
// Limit reset time: "resets at 2:00 AM" or "resets at 14:30"
|
|
13
|
+
const RESET_AT_RE = /resets?\s+(?:at\s+)?(\d{1,2}:\d{2}\s*(?:am|pm)?)/i;
|
|
13
14
|
|
|
14
15
|
class Watcher {
|
|
15
16
|
constructor(session, opts = {}) {
|
|
@@ -66,6 +67,21 @@ class Watcher {
|
|
|
66
67
|
return this.fallbackWait;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
_parseResetTime(text) {
|
|
71
|
+
// "resets at 2:00 AM"
|
|
72
|
+
const atMatch = text.match(RESET_AT_RE);
|
|
73
|
+
if (atMatch) return atMatch[1].trim().toUpperCase();
|
|
74
|
+
// "try again in X minutes" — calculate clock time
|
|
75
|
+
const inMatch = text.match(/(?:try again|retry|wait).*?in\s+(\d+)\s*(second|minute|hour)/i);
|
|
76
|
+
if (inMatch) {
|
|
77
|
+
const v = parseInt(inMatch[1]);
|
|
78
|
+
const mult = inMatch[2].startsWith('second') ? 1 : inMatch[2].startsWith('minute') ? 60 : 3600;
|
|
79
|
+
const resetMs = Date.now() + v * mult * 1000;
|
|
80
|
+
return new Date(resetMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
69
85
|
async _check() {
|
|
70
86
|
if (this._busy) return;
|
|
71
87
|
this._busy = true;
|
|
@@ -85,7 +101,13 @@ class Watcher {
|
|
|
85
101
|
const lastLine = nonEmptyLines[nonEmptyLines.length - 1] || '';
|
|
86
102
|
const recentLines = nonEmptyLines.slice(-5).join('\n');
|
|
87
103
|
|
|
88
|
-
//
|
|
104
|
+
// Extract token usage from footer whenever Claude is running
|
|
105
|
+
const tokenMatch = recentLines.match(TOKEN_RE);
|
|
106
|
+
if (tokenMatch) {
|
|
107
|
+
this.session.tokens = { sent: tokenMatch[1], received: tokenMatch[2] };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Track output stability
|
|
89
111
|
const outputHash = this._hash(recentLines);
|
|
90
112
|
if (outputHash === this._prevOutputHash) {
|
|
91
113
|
this._outputUnchangedCount++;
|
|
@@ -128,13 +150,16 @@ class Watcher {
|
|
|
128
150
|
if ((Date.now() / 1000) - this.lastResumeAt < this.cooldown) return;
|
|
129
151
|
|
|
130
152
|
this.lastHash = hash;
|
|
131
|
-
this._outputUnchangedCount = 0;
|
|
153
|
+
this._outputUnchangedCount = 0;
|
|
132
154
|
const wait = this._parseWait(text);
|
|
155
|
+
const resetTime = this._parseResetTime(text);
|
|
156
|
+
|
|
133
157
|
this.session.status = 'limit';
|
|
134
158
|
this.session.resumeAt = Date.now() + wait * 1000;
|
|
159
|
+
this.session.resetTime = resetTime;
|
|
135
160
|
|
|
136
161
|
notifier.send(this.telegram.token, this.telegram.chatId,
|
|
137
|
-
`Pilot: limit in "${this.session.name}".
|
|
162
|
+
`Pilot: limit in "${this.session.name}". Resets ${resetTime || `in ${Math.ceil(wait / 60)}m`}.`);
|
|
138
163
|
|
|
139
164
|
await new Promise(r => setTimeout(r, wait * 1000));
|
|
140
165
|
|
|
@@ -144,6 +169,7 @@ class Watcher {
|
|
|
144
169
|
this.lastResumeAt = Date.now() / 1000;
|
|
145
170
|
this.session.status = 'running';
|
|
146
171
|
this.session.resumeAt = null;
|
|
172
|
+
this.session.resetTime = null;
|
|
147
173
|
|
|
148
174
|
notifier.send(this.telegram.token, this.telegram.chatId,
|
|
149
175
|
`Pilot: resumed "${this.session.name}".`);
|
package/package.json
CHANGED