claude-code-remote-pilot 0.2.7 → 0.2.9

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.
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const fs = require('fs');
7
7
  const readline = require('readline');
8
8
  const SessionManager = require('../lib/SessionManager');
9
+ const config = require('../lib/config');
9
10
 
10
11
  // ─── dependency checks ────────────────────────────────────────────────────────
11
12
 
@@ -60,12 +61,18 @@ async function setupTelegram(rl) {
60
61
  if (process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_CHAT_ID) {
61
62
  return { token: process.env.TELEGRAM_BOT_TOKEN, chatId: process.env.TELEGRAM_CHAT_ID };
62
63
  }
64
+ const saved = config.load().telegram;
65
+ if (saved && saved.token && saved.chatId) {
66
+ console.log(' Telegram: using saved config.\n');
67
+ return saved;
68
+ }
63
69
  console.log('\nTelegram notifications (optional).');
64
70
  const answer = await question(rl, 'Set up Telegram now? (y/n) ');
65
71
  if (answer !== 'y' && answer !== 'yes') { console.log('Skipping.\n'); return {}; }
66
72
  const token = await questionRaw(rl, 'Bot token: ');
67
73
  const chatId = await questionRaw(rl, 'Chat ID: ');
68
- console.log('Telegram configured.\n');
74
+ config.saveTelegram(token, chatId);
75
+ console.log(' Telegram configured and saved.\n');
69
76
  return { token, chatId };
70
77
  }
71
78
 
@@ -151,14 +158,17 @@ function startWatch(manager, rl) {
151
158
  async function handleExit(manager, rl) {
152
159
  const sessions = manager.list();
153
160
  if (!sessions.length) {
161
+ config.clearSessions();
154
162
  console.log('');
155
163
  process.exit(0);
156
164
  }
157
165
  const answer = await questionRaw(rl, `\n Kill all ${sessions.length} session(s) before exiting? (y/n) `);
158
166
  if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
159
167
  manager.killAll();
168
+ config.clearSessions();
160
169
  console.log(' All sessions killed.\n');
161
170
  } else {
171
+ config.saveSessions(sessions);
162
172
  console.log(' Sessions keep running. Use tmux to attach.\n');
163
173
  }
164
174
  process.exit(0);
@@ -172,6 +182,7 @@ const HELP = `
172
182
  watch Live session monitor (q to exit)
173
183
  attach <name> Open tmux session in this terminal
174
184
  kill <name> Stop a session
185
+ resume [message] Show or set the message sent after a limit resets
175
186
  help Show this help
176
187
  exit Quit pilot (asks whether to kill sessions)
177
188
  `;
@@ -230,6 +241,17 @@ function startREPL(manager) {
230
241
  console.log(` ✓ "${args[0]}" killed.`);
231
242
  break;
232
243
  }
244
+ case 'resume': {
245
+ if (args.length) {
246
+ const cmd = args.join(' ');
247
+ manager.resumeCommand = cmd;
248
+ config.saveResumeCommand(cmd);
249
+ console.log(` ✓ Resume message saved: "${cmd}"`);
250
+ } else {
251
+ console.log(` Current resume message: "${manager.resumeCommand || '(default)'}"`);
252
+ }
253
+ break;
254
+ }
233
255
  case 'help': {
234
256
  console.log(HELP);
235
257
  break;
@@ -277,7 +299,27 @@ ${HELP}`);
277
299
  await ensureDep(setupRl, 'claude', 'Claude Code CLI', 'npm install -g @anthropic-ai/claude-code');
278
300
  const telegram = await setupTelegram(setupRl);
279
301
 
280
- const manager = new SessionManager({ telegram });
302
+ const cfg = config.load();
303
+ const manager = new SessionManager({ telegram, resumeCommand: cfg.resumeCommand });
304
+
305
+ // Recover sessions from previous run
306
+ const savedSessions = (cfg.sessions || []).filter(s => {
307
+ try { execSync(`tmux has-session -t "${s.name}"`, { stdio: 'ignore' }); return true; }
308
+ catch { return false; }
309
+ });
310
+
311
+ if (savedSessions.length) {
312
+ console.log(`\n Found ${savedSessions.length} session(s) still running from last time:`);
313
+ savedSessions.forEach(s => console.log(` ${s.name.padEnd(22)} ${s.path}`));
314
+ const recover = await question(setupRl, ' Re-adopt and watch them? (y/n) ');
315
+ if (recover === 'y' || recover === 'yes') {
316
+ savedSessions.forEach(s => {
317
+ try { manager.adopt(s.name, s.path); console.log(` ✓ Re-adopted "${s.name}"`); }
318
+ catch (e) { console.log(` ✗ Could not adopt "${s.name}": ${e.message}`); }
319
+ });
320
+ console.log('');
321
+ }
322
+ }
281
323
 
282
324
  const cwd = process.cwd();
283
325
  const defaultName = path.basename(cwd);
@@ -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,17 +41,25 @@ 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
 
50
+ adopt(name, dirPath) {
51
+ try { execSync(`tmux has-session -t "${name}"`, { stdio: 'ignore' }); }
52
+ catch { throw new Error(`tmux session "${name}" not found.`); }
53
+
54
+ if (this.sessions.has(name)) throw new Error(`Session "${name}" already being watched.`);
55
+
56
+ const session = { name, path: dirPath, status: 'running', startedAt: new Date(), resumeAt: null };
57
+ const watcher = this._makeWatcher(session);
58
+ watcher.start();
59
+ this.sessions.set(name, { session, watcher });
60
+ return session;
61
+ }
62
+
46
63
  kill(name) {
47
64
  const entry = this.sessions.get(name);
48
65
  if (!entry) throw new Error(`Session "${name}" not found.`);
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,6 +20,7 @@ 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;
@@ -73,17 +74,20 @@ class Watcher {
73
74
  }
74
75
 
75
76
  const text = this._capture();
76
- const lastLine = text.split('\n').filter(l => l.trim()).pop() || '';
77
+ const nonEmptyLines = text.split('\n').filter(l => l.trim());
78
+ const lastLine = nonEmptyLines[nonEmptyLines.length - 1] || '';
79
+ // Only check recent lines for transient UI elements
80
+ const recentLines = nonEmptyLines.slice(-5).join('\n');
77
81
 
78
82
  if (LIMIT_RE.test(text)) {
79
83
  await this._handleLimit(text);
80
- } else if (RESPONSE_RE.test(text)) {
84
+ } else if (RESPONSE_RE.test(recentLines)) {
81
85
  if (this.session.status !== 'needs-response') {
82
86
  this.session.status = 'needs-response';
83
87
  notifier.send(this.telegram.token, this.telegram.chatId,
84
88
  `Pilot: "${this.session.name}" needs your response.`);
85
89
  }
86
- } else if (RUNNING_RE.test(text)) {
90
+ } else if (RUNNING_RE.test(recentLines)) {
87
91
  if (this.session.status !== 'running') {
88
92
  this.session.status = 'running';
89
93
  this.session.resumeAt = null;
@@ -117,7 +121,7 @@ class Watcher {
117
121
 
118
122
  await new Promise(r => setTimeout(r, wait * 1000));
119
123
 
120
- try { execSync(`tmux send-keys -t "${this.session.name}" "continue" Enter`, { stdio: 'ignore' }); }
124
+ try { execSync(`tmux send-keys -t "${this.session.name}" "${this.resumeCommand}" Enter`, { stdio: 'ignore' }); }
121
125
  catch {}
122
126
 
123
127
  this.lastResumeAt = Date.now() / 1000;
package/lib/config.js ADDED
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ const CONFIG_PATH = path.join(os.homedir(), '.claude-remote-pilot.json');
7
+
8
+ function load() {
9
+ try {
10
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
11
+ } catch {
12
+ return {};
13
+ }
14
+ }
15
+
16
+ function save(data) {
17
+ const current = load();
18
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify({ ...current, ...data }, null, 2));
19
+ }
20
+
21
+ function saveTelegram(token, chatId) {
22
+ save({ telegram: { token, chatId } });
23
+ }
24
+
25
+ function saveSessions(sessions) {
26
+ save({ sessions: sessions.map(s => ({ name: s.name, path: s.path })) });
27
+ }
28
+
29
+ function clearSessions() {
30
+ save({ sessions: [] });
31
+ }
32
+
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.7",
3
+ "version": "0.2.9",
4
4
  "description": "Interactive Claude Code supervisor — spawn and monitor multiple Claude sessions from a single terminal.",
5
5
  "type": "commonjs",
6
6
  "bin": {