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.
@@ -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? (y/n) ');
39
- if (answer !== 'y' && answer !== 'yes') {
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/n) ');
71
- if (answer !== 'y' && answer !== 'yes') { console.log('Skipping.\n'); return {}; }
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 = 8;
123
- const bar = ' ' + '─'.repeat(NW + SW + UW + 34);
124
- const header = ` ${'SESSION'.padEnd(NW)} ${'STATUS'.padEnd(SW)} ${'UP'.padEnd(UW)} DIRECTORY`;
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
- return ` ${trunc(s.name, NW)} ${colored}${pad} ${uptime(s.startedAt).padEnd(UW)} ${trunc(s.path, 32)}`;
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 questionRaw(rl, `\n Kill all ${sessions.length} session(s) before exiting? (y/n) `);
173
- if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
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? (y/n) ');
322
- if (recover === 'y' || recover === 'yes') {
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}) [y/n] `);
347
+ const mount = await question(setupRl, `Mount current directory as a session? (${defaultName}) [Y/n] `);
334
348
 
335
- if (mount === 'y' || mount === 'yes') {
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
- // Track whether output is changing between checks
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; // reset — we're intentionally waiting
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}". Waiting ${wait}s before resume.`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-remote-pilot",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Interactive Claude Code supervisor — spawn and monitor multiple Claude sessions from a single terminal.",
5
5
  "type": "commonjs",
6
6
  "bin": {