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.
@@ -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 = 20, SW = 14;
116
- const bar = ' ' + '─'.repeat(NW + SW + 42);
117
- const header = ` ${'SESSION'.padEnd(NW)} ${'STATUS'.padEnd(SW)} DIRECTORY`;
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
- return ` ${trunc(s.name, NW)} ${colored}${pad} ${trunc(s.path, 40)}`;
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 text = this._capture();
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}". Waiting ${wait}s before resume.`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-remote-pilot",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Interactive Claude Code supervisor — spawn and monitor multiple Claude sessions from a single terminal.",
5
5
  "type": "commonjs",
6
6
  "bin": {