claude-code-remote-pilot 0.2.9 → 0.2.11
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 +22 -4
- package/lib/Watcher.js +50 -7
- package/package.json +1 -1
package/bin/claude-pilot.js
CHANGED
|
@@ -111,14 +111,32 @@ 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
|
+
|
|
121
|
+
function formatUsage(session) {
|
|
122
|
+
if (session.status === 'limit' && session.resetTime) {
|
|
123
|
+
return `resets ${session.resetTime}`;
|
|
124
|
+
}
|
|
125
|
+
if (session.tokens) {
|
|
126
|
+
return `↑${session.tokens.sent} ↓${session.tokens.received}`;
|
|
127
|
+
}
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
|
|
114
131
|
function renderTable(sessions) {
|
|
115
|
-
const NW =
|
|
116
|
-
const bar = ' ' + '─'.repeat(NW + SW +
|
|
117
|
-
const header = ` ${'SESSION'.padEnd(NW)} ${'STATUS'.padEnd(SW)}
|
|
132
|
+
const NW = 18, SW = 14, UW = 7, TW = 16;
|
|
133
|
+
const bar = ' ' + '─'.repeat(NW + SW + UW + TW + 10);
|
|
134
|
+
const header = ` ${'SESSION'.padEnd(NW)} ${'STATUS'.padEnd(SW)} ${'UP'.padEnd(UW)} ${'USAGE / RESET'.padEnd(TW)}`;
|
|
118
135
|
const rows = sessions.map(s => {
|
|
119
136
|
const { plain, colored } = formatStatus(s);
|
|
120
137
|
const pad = ' '.repeat(Math.max(0, SW - plain.length));
|
|
121
|
-
|
|
138
|
+
const usage = formatUsage(s);
|
|
139
|
+
return ` ${trunc(s.name, NW)} ${colored}${pad} ${uptime(s.startedAt).padEnd(UW)} ${trunc(usage, TW)}`;
|
|
122
140
|
});
|
|
123
141
|
const footer = ` ${sessions.length} session${sessions.length !== 1 ? 's' : ''} ${new Date().toLocaleTimeString()} q to exit`;
|
|
124
142
|
return ['\n', ' Claude Code Remote Pilot', bar, header, bar, ...rows, bar, footer, ''].join('\n');
|
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 = {}) {
|
|
@@ -25,6 +26,12 @@ class Watcher {
|
|
|
25
26
|
this.lastResumeAt = 0;
|
|
26
27
|
this._timer = null;
|
|
27
28
|
this._busy = false;
|
|
29
|
+
this._prevOutputHash = '';
|
|
30
|
+
this._outputUnchangedCount = 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_stripAnsi(text) {
|
|
34
|
+
return text.replace(/\x1b\[[0-9;]*[mGKHFABCDJsuhl]/g, '').replace(/\x1b[()][AB012]/g, '');
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
start() {
|
|
@@ -60,6 +67,21 @@ class Watcher {
|
|
|
60
67
|
return this.fallbackWait;
|
|
61
68
|
}
|
|
62
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
|
+
|
|
63
85
|
async _check() {
|
|
64
86
|
if (this._busy) return;
|
|
65
87
|
this._busy = true;
|
|
@@ -73,12 +95,28 @@ class Watcher {
|
|
|
73
95
|
return;
|
|
74
96
|
}
|
|
75
97
|
|
|
76
|
-
const
|
|
98
|
+
const raw = this._capture();
|
|
99
|
+
const text = this._stripAnsi(raw);
|
|
77
100
|
const nonEmptyLines = text.split('\n').filter(l => l.trim());
|
|
78
101
|
const lastLine = nonEmptyLines[nonEmptyLines.length - 1] || '';
|
|
79
|
-
// Only check recent lines for transient UI elements
|
|
80
102
|
const recentLines = nonEmptyLines.slice(-5).join('\n');
|
|
81
103
|
|
|
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
|
|
111
|
+
const outputHash = this._hash(recentLines);
|
|
112
|
+
if (outputHash === this._prevOutputHash) {
|
|
113
|
+
this._outputUnchangedCount++;
|
|
114
|
+
} else {
|
|
115
|
+
this._prevOutputHash = outputHash;
|
|
116
|
+
this._outputUnchangedCount = 0;
|
|
117
|
+
}
|
|
118
|
+
const outputIsStable = this._outputUnchangedCount >= 2;
|
|
119
|
+
|
|
82
120
|
if (LIMIT_RE.test(text)) {
|
|
83
121
|
await this._handleLimit(text);
|
|
84
122
|
} else if (RESPONSE_RE.test(recentLines)) {
|
|
@@ -92,7 +130,7 @@ class Watcher {
|
|
|
92
130
|
this.session.status = 'running';
|
|
93
131
|
this.session.resumeAt = null;
|
|
94
132
|
}
|
|
95
|
-
} else if (IDLE_RE.test(lastLine)) {
|
|
133
|
+
} else if (IDLE_RE.test(lastLine) || outputIsStable) {
|
|
96
134
|
if (this.session.status !== 'idle') {
|
|
97
135
|
this.session.status = 'idle';
|
|
98
136
|
this.session.resumeAt = null;
|
|
@@ -112,12 +150,16 @@ class Watcher {
|
|
|
112
150
|
if ((Date.now() / 1000) - this.lastResumeAt < this.cooldown) return;
|
|
113
151
|
|
|
114
152
|
this.lastHash = hash;
|
|
153
|
+
this._outputUnchangedCount = 0;
|
|
115
154
|
const wait = this._parseWait(text);
|
|
155
|
+
const resetTime = this._parseResetTime(text);
|
|
156
|
+
|
|
116
157
|
this.session.status = 'limit';
|
|
117
158
|
this.session.resumeAt = Date.now() + wait * 1000;
|
|
159
|
+
this.session.resetTime = resetTime;
|
|
118
160
|
|
|
119
161
|
notifier.send(this.telegram.token, this.telegram.chatId,
|
|
120
|
-
`Pilot: limit in "${this.session.name}".
|
|
162
|
+
`Pilot: limit in "${this.session.name}". Resets ${resetTime || `in ${Math.ceil(wait / 60)}m`}.`);
|
|
121
163
|
|
|
122
164
|
await new Promise(r => setTimeout(r, wait * 1000));
|
|
123
165
|
|
|
@@ -127,6 +169,7 @@ class Watcher {
|
|
|
127
169
|
this.lastResumeAt = Date.now() / 1000;
|
|
128
170
|
this.session.status = 'running';
|
|
129
171
|
this.session.resumeAt = null;
|
|
172
|
+
this.session.resetTime = null;
|
|
130
173
|
|
|
131
174
|
notifier.send(this.telegram.token, this.telegram.chatId,
|
|
132
175
|
`Pilot: resumed "${this.session.name}".`);
|
package/package.json
CHANGED