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.
@@ -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 = 20, SW = 14;
116
- const bar = ' ' + '─'.repeat(NW + SW + 42);
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, 40)}`;
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 manager = new SessionManager({ telegram });
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 = (config.load().sessions || []).filter(s => {
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
  });
@@ -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 = new Watcher(session, {
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
- // Claude Code's permission/action prompt UI
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
- // Claude Code shows this footer while actively processing
9
+ // Checked against last 5 lines only — footer disappears when Claude finishes
10
10
  const RUNNING_RE = /esc to interrupt/i;
11
- // Claude Code shows ">" alone on the last line when idle/waiting for next prompt
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 text = this._capture();
76
- const lastLine = text.split('\n').filter(l => l.trim()).pop() || '';
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(text)) {
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(text)) {
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}" "continue" Enter`, { stdio: 'ignore' }); }
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
- module.exports = { load, saveTelegram, saveSessions, clearSessions };
33
+ function saveResumeCommand(cmd) {
34
+ save({ resumeCommand: cmd });
35
+ }
36
+
37
+ module.exports = { load, saveTelegram, saveSessions, clearSessions, saveResumeCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-remote-pilot",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Interactive Claude Code supervisor — spawn and monitor multiple Claude sessions from a single terminal.",
5
5
  "type": "commonjs",
6
6
  "bin": {