claude-code-remote-pilot 0.1.5 → 0.2.1

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/README.md CHANGED
@@ -1,388 +1,184 @@
1
1
  # Claude Code Remote Pilot
2
2
 
3
- Interactive Claude Code supervisor using `tmux`, Node.js, and notifications.
3
+ Spawn and supervise multiple Claude Code sessions from a single interactive terminal.
4
4
 
5
- The current MVP keeps Claude Code usable in the normal terminal while adding a small supervisor layer that can detect usage limits, notify you, wait, and send `continue` automatically.
5
+ Run it once. Spawn Claude into as many project directories as you want. Walk away it handles usage limits, waits for resets, sends `continue` automatically, and notifies you on Telegram. Come back to finished work.
6
6
 
7
- Longer term, this project is moving toward a local multi-session Claude Code dashboard.
7
+ Each Claude session lives in its own named tmux session. You can `tmux attach -t <name>` from any terminal at any time, independently of the pilot.
8
8
 
9
9
  ---
10
10
 
11
- ## Current Status
11
+ ## How it works
12
12
 
13
- This repository currently contains:
14
-
15
- - `claude-pilot.sh` — bash/tmux watcher MVP
16
- - `bin/claude-pilot.js` — npm CLI wrapper
17
- - `package.json` — npm package entry
18
- - `docs/ARCHITECTURE_UPDATE.md` — architecture direction
19
- - `TASKS.md` — implementation roadmap
20
-
21
- Current mode:
22
-
23
- ```text
24
- Human → tmux terminal → Claude Code
25
-
26
-
27
- Claude Code Remote Pilot
28
- - watches output
29
- - detects limits
30
- - sends notifications
31
- - resumes automatically
32
13
  ```
14
+ npx claude-code-remote-pilot
15
+
16
+ ├── asks: mount current directory as a session?
17
+ ├── asks: set up Telegram? (optional)
18
+ └── starts interactive prompt
33
19
 
34
- Future mode:
35
-
36
- ```text
37
- Browser Dashboard
38
-
39
- WebSocket / REST API
40
-
41
- Node.js Supervisor
42
-
43
- Session Manager
44
-
45
- tmux sessions
46
-
47
- Claude Code instances
48
- ```
49
-
50
- ---
51
-
52
- ## Why This Exists
53
-
54
- Claude Code is useful for long-running coding tasks, but usage/rate limits can interrupt the flow.
55
-
56
- This project aims to provide:
57
-
58
- - persistent Claude Code sessions
59
- - automatic resume after limit reset
60
- - human-supervised automation
61
- - local-first workflow
62
- - multi-session management
63
- - eventual web dashboard control
64
-
65
- It is intentionally **not** designed as an unsafe fully autonomous agent loop.
66
-
67
- ---
68
-
69
- ## Features
70
-
71
- Current MVP:
72
-
73
- - tmux session persistence
74
- - auto-create Claude tmux session
75
- - terminal output capture
76
- - usage/rate limit detection
77
- - reset-time parsing
78
- - Telegram notification support
79
- - automatic `continue` after waiting
80
- - duplicate event protection
81
- - resume cooldown protection
82
- - npm CLI wrapper
83
-
84
- Planned:
85
-
86
- - Node.js runtime refactor
87
- - multiple Claude sessions
88
- - web dashboard
89
- - WebSocket live output
90
- - session registry
91
- - pluggable detectors
92
- - notification providers
93
- - persistent session state
94
- - policy/safety engine
95
-
96
- ---
97
-
98
- ## Requirements
99
-
100
- - Node.js >= 18
101
- - bash
102
- - tmux
103
- - curl
104
- - python3 recommended for better reset-time parsing
105
- - Claude Code CLI installed and authenticated
106
-
107
- ---
108
-
109
- ## Installing tmux
110
-
111
- **macOS (Homebrew):**
112
-
113
- ```bash
114
- brew install tmux
20
+ claude-pilot> spawn ~/projects/api-refactor
21
+ claude-pilot> spawn ~/projects/mobile-app
22
+ claude-pilot> watch
115
23
  ```
116
24
 
117
- **Ubuntu / Debian:**
118
-
119
- ```bash
120
- sudo apt update && sudo apt install tmux curl python3
121
25
  ```
122
-
123
- **Fedora / RHEL / CentOS:**
124
-
125
- ```bash
126
- sudo dnf install tmux curl python3
26
+ Claude Code Remote Pilot
27
+ ─────────────────────────────────────────────────────────────────
28
+ SESSION STATUS DIRECTORY
29
+ ─────────────────────────────────────────────────────────────────
30
+ api-refactor running ~/projects/api-refactor
31
+ mobile-app limit 42m ~/projects/mobile-app
32
+ ─────────────────────────────────────────────────────────────────
33
+ 2 sessions 14:23:05 q to exit
127
34
  ```
128
35
 
129
- **Arch Linux:**
36
+ From any other terminal, interact with a session directly:
130
37
 
131
38
  ```bash
132
- sudo pacman -S tmux curl python3
39
+ tmux attach -t api-refactor
40
+ # Ctrl+B then D to detach back to your terminal
133
41
  ```
134
42
 
135
- **Windows (WSL):**
43
+ ---
136
44
 
137
- Run inside your WSL terminal (Ubuntu recommended):
45
+ ## Install
138
46
 
139
47
  ```bash
140
- sudo apt update && sudo apt install tmux curl python3
48
+ npx claude-code-remote-pilot
141
49
  ```
142
50
 
143
- Verify tmux is working:
51
+ Or install globally:
144
52
 
145
53
  ```bash
146
- tmux -V
54
+ npm install -g claude-code-remote-pilot
55
+ claude-remote-pilot
147
56
  ```
148
57
 
149
58
  ---
150
59
 
151
- ## Install
60
+ ## Requirements
152
61
 
153
- ### From GitHub
62
+ - Node.js >= 18
63
+ - tmux
64
+ - Claude Code CLI (`npm install -g @anthropic-ai/claude-code`)
154
65
 
155
- ```bash
156
- git clone https://github.com/mekku/claude-code-remote-pilot.git
157
- cd claude-code-remote-pilot
158
- yarn install
159
- ```
66
+ The pilot will prompt you to install missing dependencies on first run.
160
67
 
161
- Run locally:
68
+ ### Installing tmux manually
162
69
 
70
+ **macOS:**
163
71
  ```bash
164
- yarn pilot
72
+ brew install tmux
165
73
  ```
166
74
 
167
- or:
168
-
75
+ **Ubuntu / Debian:**
169
76
  ```bash
170
- node bin/claude-pilot.js
77
+ sudo apt update && sudo apt install tmux
171
78
  ```
172
79
 
173
- ### As npm package
174
-
80
+ **Fedora / RHEL:**
175
81
  ```bash
176
- npm install -g claude-code-remote-pilot
177
- claude-remote-pilot
82
+ sudo dnf install tmux
178
83
  ```
179
84
 
180
- or:
181
-
85
+ **Arch:**
182
86
  ```bash
183
- npx claude-code-remote-pilot
87
+ sudo pacman -S tmux
184
88
  ```
185
89
 
186
- ---
187
-
188
- ## Telegram Setup
189
-
190
- Create a bot with `@BotFather` and get your bot token.
191
-
192
- Send a message to your bot, then get your chat ID:
193
-
90
+ **Windows (WSL):**
194
91
  ```bash
195
- curl "https://api.telegram.org/bot<BOT_TOKEN>/getUpdates"
196
- ```
197
-
198
- Look for:
199
-
200
- ```json
201
- "chat": {
202
- "id": 123456789
203
- }
92
+ sudo apt update && sudo apt install tmux
204
93
  ```
205
94
 
206
- Run:
207
-
208
- ```bash
209
- export TELEGRAM_BOT_TOKEN="xxx"
210
- export TELEGRAM_CHAT_ID="123456789"
95
+ ---
211
96
 
212
- yarn pilot
213
- ```
97
+ ## Commands
214
98
 
215
- Telegram is optional. If not configured, messages are printed locally.
99
+ | Command | Description |
100
+ |---|---|
101
+ | `spawn <path> [name]` | Start Claude at a path. Name defaults to the directory name. |
102
+ | `list` | One-shot status of all sessions. |
103
+ | `watch` | Live-updating session monitor. Press `q` to exit. |
104
+ | `attach <name>` | Open a tmux session in the current terminal. |
105
+ | `kill <name>` | Stop a session. |
106
+ | `help` | Show command reference. |
107
+ | `exit` | Quit the pilot. Sessions keep running in tmux. |
216
108
 
217
109
  ---
218
110
 
219
- ## Basic Usage
111
+ ## Telegram setup
220
112
 
221
- Start pilot:
113
+ Create a bot via `@BotFather` and get your token.
222
114
 
223
- ```bash
224
- yarn pilot
225
- ```
226
-
227
- Attach to Claude:
115
+ Get your chat ID:
228
116
 
229
117
  ```bash
230
- tmux attach -t claude
118
+ curl "https://api.telegram.org/bot<TOKEN>/getUpdates"
231
119
  ```
232
120
 
233
- Detach without killing Claude:
121
+ Run the pilot — it will ask for these values interactively, or set them as environment variables:
234
122
 
235
- ```text
236
- Ctrl+B then D
123
+ ```bash
124
+ export TELEGRAM_BOT_TOKEN="your-token"
125
+ export TELEGRAM_CHAT_ID="your-chat-id"
126
+ npx claude-code-remote-pilot
237
127
  ```
238
128
 
239
- The watcher keeps running and watches the tmux session.
240
-
241
129
  ---
242
130
 
243
- ## Environment Variables
131
+ ## Environment variables
244
132
 
245
133
  | Variable | Default | Description |
246
- |---|---:|---|
247
- | `TELEGRAM_BOT_TOKEN` | empty | Telegram bot token |
248
- | `TELEGRAM_CHAT_ID` | empty | Telegram chat ID |
249
- | `CLAUDE_SESSION` | `claude` | tmux session name |
250
- | `CLAUDE_COMMAND` | `claude` | command used to start Claude |
251
- | `CHECK_INTERVAL_SECONDS` | `30` | watcher interval |
252
- | `LIMIT_FALLBACK_WAIT_SECONDS` | `300` | fallback wait if reset time cannot be parsed |
253
- | `POST_RESUME_COOLDOWN_SECONDS` | `180` | avoid repeated resume spam |
254
- | `CAPTURE_LINES` | `500` | number of tmux output lines to inspect |
255
- | `RESUME_COMMAND` | `continue` | command sent after reset |
256
- | `START_IF_MISSING` | `1` | auto-create tmux session if missing |
257
- | `LIMIT_REGEX` | built-in | custom regex for limit detection |
258
- | `PERMISSION_REGEX` | built-in | custom regex for permission detection |
259
-
260
- Example:
261
-
262
- ```bash
263
- CLAUDE_SESSION=claude-buildx-api \
264
- CLAUDE_COMMAND="claude" \
265
- TELEGRAM_BOT_TOKEN="xxx" \
266
- TELEGRAM_CHAT_ID="123456789" \
267
- yarn pilot
268
- ```
134
+ |---|---|---|
135
+ | `TELEGRAM_BOT_TOKEN` | | Telegram bot token |
136
+ | `TELEGRAM_CHAT_ID` | | Telegram chat ID |
137
+ | `CLAUDE_COMMAND` | `claude` | Command used to start Claude |
269
138
 
270
139
  ---
271
140
 
272
- ## Recommended Claude Workflow
141
+ ## Recommended Claude workflow
273
142
 
274
- For long-running work, ask Claude to maintain external state:
143
+ For long-running tasks, ask Claude to keep external state:
275
144
 
276
- ```text
145
+ ```
277
146
  Maintain TASK_STATE.md.
278
- After every meaningful step:
279
- - update completed work
280
- - update current status
281
- - update next exact action
282
-
283
- If interrupted:
284
- - read TASK_STATE.md
285
- - resume from the next unfinished step.
147
+ After every meaningful step update: what's done, current status, next exact action.
148
+ If interrupted, read TASK_STATE.md and resume from where you left off.
286
149
  ```
287
150
 
288
- This matters because LLM context can drift over long sessions. External state is the real resume brain.
151
+ Context can drift over long sessions. External state is the real resume brain.
289
152
 
290
153
  ---
291
154
 
292
- ## Safety Notes
155
+ ## Safety
293
156
 
294
- Avoid starting with:
157
+ Start Claude without `--dangerously-skip-permissions` unless you know what you're doing. The pilot is designed for human-supervised workflows:
295
158
 
296
- ```bash
297
- claude --dangerously-skip-permissions
298
- ```
299
-
300
- That mode allows Claude to execute commands and modify files without asking.
301
-
302
- Claude Code Pilot should begin as a **human-supervised** tool:
303
-
304
- Allowed early:
305
-
306
- - watch output
307
- - notify
308
- - send `continue`
309
- - send user-provided input
310
- - show latest results
311
-
312
- Avoid early:
313
-
314
- - auto-approve permissions
315
- - arbitrary remote shell execution
316
- - autonomous destructive actions
159
+ - watches output
160
+ - sends notifications
161
+ - sends `continue` after limit resets
162
+ - does **not** auto-approve permissions or execute arbitrary commands
317
163
 
318
164
  ---
319
165
 
320
166
  ## Roadmap
321
167
 
322
- See:
323
-
324
- - [`docs/ARCHITECTURE_UPDATE.md`](docs/ARCHITECTURE_UPDATE.md)
325
- - [`TASKS.md`](TASKS.md)
326
-
327
- High-level phases:
328
-
329
- 1. tmux watcher MVP
330
- 2. Node.js runtime refactor
331
- 3. multi-session support
332
- 4. detection engine
333
- 5. notification providers
334
- 6. web dashboard
335
- 7. persistent state
336
- 8. safety/policy engine
337
- 9. optional node-pty backend
338
-
339
- ---
340
-
341
- ## Target Dashboard Concept
342
-
343
- Planned local dashboard:
344
-
345
- ```text
346
- Sidebar
347
- ├── claude-buildx-api
348
- ├── claude-pos-mobile
349
- ├── claude-auth-refactor
350
- └── claude-research
351
-
352
- Main Panel
353
- ├── live output
354
- ├── status badge
355
- ├── input box
356
- ├── continue button
357
- └── auto-resume toggle
358
- ```
359
-
360
- This allows normal local terminal interaction when at the machine, plus remote-lite control when away.
168
+ - [x] tmux session management
169
+ - [x] usage limit detection and auto-resume
170
+ - [x] Telegram notifications
171
+ - [x] interactive REPL — spawn, watch, attach, kill
172
+ - [x] multi-session support
173
+ - [ ] web dashboard (sessions connect to pilot server)
174
+ - [ ] persistent session state
175
+ - [ ] pluggable notification providers
176
+ - [ ] safety / policy engine
361
177
 
362
178
  ---
363
179
 
364
180
  ## Philosophy
365
181
 
366
- Claude Code Pilot is intended to become:
367
-
368
- ```text
369
- human-supervised local AI runtime
370
- ```
371
-
372
- not:
373
-
374
- ```text
375
- fully autonomous AI operator
376
- ```
377
-
378
- The system prioritizes:
379
-
380
- - persistence
381
- - observability
382
- - recoverability
383
- - resumability
384
- - multi-session control
385
- - human intervention
386
- - practical workflows
182
+ A human-supervised local runtime for Claude Code. Not a fully autonomous agent loop.
387
183
 
388
- Small tools first. Spaceship later.
184
+ Small tools first. Dashboard later.
@@ -1,31 +1,24 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
2
3
 
3
- const { spawn, execSync } = require('child_process');
4
+ const { execSync, spawn } = require('child_process');
4
5
  const path = require('path');
5
6
  const fs = require('fs');
6
-
7
- const scriptPath = path.resolve(__dirname, '..', 'claude-pilot.sh');
8
-
9
- if (!fs.existsSync(scriptPath)) {
10
- console.error('claude-pilot.sh not found');
11
- process.exit(1);
12
- }
13
-
14
7
  const readline = require('readline');
8
+ const SessionManager = require('../lib/SessionManager');
9
+
10
+ // ─── dependency checks ────────────────────────────────────────────────────────
15
11
 
16
- function checkDep(cmd) {
17
- try { execSync(`command -v ${cmd}`, { stdio: 'ignore' }); return true; }
18
- catch { return false; }
12
+ function has(cmd) {
13
+ try { execSync(`command -v ${cmd}`, { stdio: 'ignore' }); return true; } catch { return false; }
19
14
  }
20
15
 
21
16
  function detectPlatform() {
22
- const platform = process.platform;
23
- if (platform === 'darwin') return 'macos';
24
- if (platform === 'win32') return 'windows';
17
+ if (process.platform === 'darwin') return 'macos';
25
18
  try {
26
- const release = fs.readFileSync('/etc/os-release', 'utf8');
27
- if (/ID=arch/i.test(release)) return 'arch';
28
- if (/ID=(fedora|rhel|centos)/i.test(release)) return 'fedora';
19
+ const r = fs.readFileSync('/etc/os-release', 'utf8');
20
+ if (/ID=arch/i.test(r)) return 'arch';
21
+ if (/ID=(fedora|rhel|centos)/i.test(r)) return 'fedora';
29
22
  } catch {}
30
23
  return 'debian';
31
24
  }
@@ -38,126 +31,241 @@ function tmuxInstallCmd() {
38
31
  return 'sudo apt-get install -y tmux';
39
32
  }
40
33
 
41
- function prompt(question) {
42
- return new Promise((resolve) => {
43
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
44
- rl.question(question, (answer) => { rl.close(); resolve(answer.trim().toLowerCase()); });
45
- });
46
- }
47
-
48
- async function ensureDep(cmd, label, installCmd) {
49
- if (checkDep(cmd)) return;
34
+ async function ensureDep(rl, cmd, label, installCmd) {
35
+ if (has(cmd)) return;
50
36
  console.log(`\n${label} is not installed.`);
51
- const answer = await prompt(`Install it now? (y/n) `);
37
+ const answer = await question(rl, 'Install it now? (y/n) ');
52
38
  if (answer !== 'y' && answer !== 'yes') {
53
- console.log(`Skipping. Run manually: ${installCmd}`);
39
+ console.log(`Run manually: ${installCmd}`);
54
40
  process.exit(1);
55
41
  }
56
42
  console.log(`Running: ${installCmd}\n`);
57
- try {
58
- execSync(installCmd, { stdio: 'inherit' });
59
- console.log(`\n${label} installed successfully.\n`);
60
- } catch {
61
- console.error(`\nInstall failed. Try running manually: ${installCmd}`);
62
- process.exit(1);
43
+ try { execSync(installCmd, { stdio: 'inherit' }); console.log(`\n${label} installed.\n`); }
44
+ catch { console.error(`Install failed. Run manually: ${installCmd}`); process.exit(1); }
45
+ }
46
+
47
+ // ─── helpers ──────────────────────────────────────────────────────────────────
48
+
49
+ function question(rl, q) {
50
+ return new Promise(r => rl.question(q, a => r(a.trim().toLowerCase())));
51
+ }
52
+
53
+ function questionRaw(rl, q) {
54
+ return new Promise(r => rl.question(q, a => r(a.trim())));
55
+ }
56
+
57
+ // ─── telegram setup ───────────────────────────────────────────────────────────
58
+
59
+ async function setupTelegram(rl) {
60
+ if (process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_CHAT_ID) {
61
+ return { token: process.env.TELEGRAM_BOT_TOKEN, chatId: process.env.TELEGRAM_CHAT_ID };
63
62
  }
63
+ console.log('\nTelegram notifications (optional).');
64
+ const answer = await question(rl, 'Set up Telegram now? (y/n) ');
65
+ if (answer !== 'y' && answer !== 'yes') { console.log('Skipping.\n'); return {}; }
66
+ const token = await questionRaw(rl, 'Bot token: ');
67
+ const chatId = await questionRaw(rl, 'Chat ID: ');
68
+ console.log('Telegram configured.\n');
69
+ return { token, chatId };
64
70
  }
65
71
 
66
- async function setupTelegram() {
67
- if (process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_CHAT_ID) return;
68
- console.log('\nTelegram notifications are not configured (optional).');
69
- const answer = await prompt('Set up Telegram now? (y/n) ');
70
- if (answer !== 'y' && answer !== 'yes') {
71
- console.log('Skipping. You can set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID later.\n');
72
- return;
72
+ // ─── table rendering ──────────────────────────────────────────────────────────
73
+
74
+ const C = {
75
+ reset: '\x1b[0m',
76
+ green: '\x1b[32m',
77
+ yellow: '\x1b[33m',
78
+ dim: '\x1b[2m',
79
+ };
80
+
81
+ function formatStatus(session) {
82
+ if (session.status === 'running') return { plain: 'running', colored: `${C.green}running${C.reset}` };
83
+ if (session.status === 'limit') {
84
+ const secs = session.resumeAt ? Math.max(0, Math.round((session.resumeAt - Date.now()) / 1000)) : 0;
85
+ const label = `limit ${Math.ceil(secs / 60)}m`;
86
+ return { plain: label, colored: `${C.yellow}${label}${C.reset}` };
73
87
  }
74
- const token = await prompt('Enter your Telegram bot token: ');
75
- const chatId = await prompt('Enter your Telegram chat ID: ');
76
- if (token) process.env.TELEGRAM_BOT_TOKEN = token.trim();
77
- if (chatId) process.env.TELEGRAM_CHAT_ID = chatId.trim();
78
- console.log('Telegram configured for this session.\n');
88
+ if (session.status === 'needs-input') return { plain: 'needs input', colored: `${C.yellow}needs input${C.reset}` };
89
+ if (session.status === 'ended') return { plain: 'ended', colored: `${C.dim}ended${C.reset}` };
90
+ return { plain: session.status, colored: session.status };
79
91
  }
80
92
 
81
- function printGuide() {
82
- const session = process.env.CLAUDE_SESSION;
83
- const workdir = process.env.CLAUDE_WORKDIR;
84
- console.log(`
85
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
86
- Claude Code Remote Pilot — Ready
87
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
93
+ function trunc(str, len) {
94
+ return str.length <= len ? str.padEnd(len) : str.slice(0, len - 1) + '…';
95
+ }
88
96
 
89
- Session : ${session}
90
- Dir : ${workdir}
97
+ function renderTable(sessions) {
98
+ const NW = 20, SW = 14;
99
+ const bar = ' ' + '─'.repeat(NW + SW + 42);
100
+ const header = ` ${'SESSION'.padEnd(NW)} ${'STATUS'.padEnd(SW)} DIRECTORY`;
101
+ const rows = sessions.map(s => {
102
+ const { plain, colored } = formatStatus(s);
103
+ const pad = ' '.repeat(Math.max(0, SW - plain.length));
104
+ return ` ${trunc(s.name, NW)} ${colored}${pad} ${trunc(s.path, 40)}`;
105
+ });
106
+ const footer = ` ${sessions.length} session${sessions.length !== 1 ? 's' : ''} ${new Date().toLocaleTimeString()} q to exit`;
107
+ return ['\n', ' Claude Code Remote Pilot', bar, header, bar, ...rows, bar, footer, ''].join('\n');
108
+ }
109
+
110
+ // ─── watch mode ───────────────────────────────────────────────────────────────
111
+
112
+ function startWatch(manager, rl) {
113
+ rl.removeAllListeners('close');
114
+ rl.close();
115
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
116
+ process.stdin.resume();
91
117
 
92
- To talk to Claude:
93
- tmux attach -t ${session}
118
+ function draw() {
119
+ process.stdout.write('\x1B[2J\x1B[0f');
120
+ process.stdout.write(renderTable(manager.list()));
121
+ }
94
122
 
95
- To detach without stopping Claude:
96
- Press Ctrl+B then D
123
+ draw();
124
+ const interval = setInterval(draw, 2000);
97
125
 
98
- The pilot will:
99
- detect usage/rate limits
100
- wait for the reset
101
- • send "continue" automatically
102
- • notify you via Telegram (if configured)
126
+ function onData(buf) {
127
+ const key = buf.toString();
128
+ if (key === 'q' || key === 'Q' || buf[0] === 3) {
129
+ clearInterval(interval);
130
+ process.stdin.removeListener('data', onData);
131
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
132
+ process.stdout.write('\x1B[2J\x1B[0f');
133
+ startREPL(manager);
134
+ }
135
+ }
103
136
 
104
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
105
- `);
137
+ process.stdin.on('data', onData);
106
138
  }
107
139
 
108
- async function main() {
109
- await ensureDep('tmux', 'tmux', tmuxInstallCmd());
110
- await ensureDep('claude', 'Claude Code CLI', 'npm install -g @anthropic-ai/claude-code');
111
- await setupTelegram();
112
- printGuide();
113
-
114
- const child = spawn('bash', [scriptPath], {
115
- stdio: 'inherit',
116
- env: process.env,
140
+ // ─── REPL ─────────────────────────────────────────────────────────────────────
141
+
142
+ const HELP = `
143
+ spawn <path> [name] Start Claude at path (name defaults to dir name)
144
+ list Show all sessions
145
+ watch Live session monitor (q to exit)
146
+ attach <name> Open tmux session in this terminal
147
+ kill <name> Stop a session
148
+ help Show this help
149
+ exit Quit pilot (sessions keep running in tmux)
150
+ `;
151
+
152
+ function startREPL(manager) {
153
+ const rl = readline.createInterface({
154
+ input: process.stdin,
155
+ output: process.stdout,
156
+ prompt: 'claude-pilot> ',
157
+ });
158
+
159
+ rl.prompt();
160
+
161
+ rl.on('line', async (line) => {
162
+ const parts = line.trim().split(/\s+/).filter(Boolean);
163
+ const [cmd, ...args] = parts;
164
+
165
+ if (!cmd) { rl.prompt(); return; }
166
+
167
+ try {
168
+ switch (cmd) {
169
+ case 'spawn': {
170
+ if (!args[0]) { console.log(' Usage: spawn <path> [name]'); break; }
171
+ const session = manager.spawn(args[0], args[1]);
172
+ console.log(` ✓ "${session.name}" started at ${session.path}`);
173
+ console.log(` tmux attach -t ${session.name}`);
174
+ break;
175
+ }
176
+ case 'list': {
177
+ const sessions = manager.list();
178
+ if (!sessions.length) { console.log(' No sessions.'); break; }
179
+ console.log('');
180
+ sessions.forEach(s => {
181
+ const { plain } = formatStatus(s);
182
+ console.log(` ${s.name.padEnd(22)} ${plain.padEnd(14)} ${s.path}`);
183
+ });
184
+ console.log('');
185
+ break;
186
+ }
187
+ case 'watch': {
188
+ const sessions = manager.list();
189
+ if (!sessions.length) { console.log(' No sessions.'); break; }
190
+ startWatch(manager, rl);
191
+ return;
192
+ }
193
+ case 'attach': {
194
+ if (!args[0]) { console.log(' Usage: attach <name>'); break; }
195
+ rl.pause();
196
+ const child = spawn('tmux', ['attach-session', '-t', args[0]], { stdio: 'inherit' });
197
+ child.on('exit', () => { process.stdout.write('\n'); rl.resume(); rl.prompt(); });
198
+ return;
199
+ }
200
+ case 'kill': {
201
+ if (!args[0]) { console.log(' Usage: kill <name>'); break; }
202
+ manager.kill(args[0]);
203
+ console.log(` ✓ "${args[0]}" killed.`);
204
+ break;
205
+ }
206
+ case 'help': {
207
+ console.log(HELP);
208
+ break;
209
+ }
210
+ case 'exit':
211
+ case 'quit': {
212
+ console.log('\n Sessions keep running. Use tmux to attach.\n');
213
+ process.exit(0);
214
+ }
215
+ default:
216
+ console.log(` Unknown command: ${cmd}. Type help.`);
217
+ }
218
+ } catch (err) {
219
+ console.error(` Error: ${err.message}`);
220
+ }
221
+
222
+ rl.prompt();
117
223
  });
118
224
 
119
- child.on('exit', (code) => {
120
- process.exit(code || 0);
225
+ rl.on('close', () => {
226
+ console.log('\n Sessions keep running. Use tmux to attach.\n');
227
+ process.exit(0);
121
228
  });
122
229
  }
123
230
 
124
- if (process.argv.includes('--help') || process.argv.includes('-h')) {
125
- console.log(`
231
+ // ─── main ─────────────────────────────────────────────────────────────────────
232
+
233
+ async function main() {
234
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
235
+ console.log(`
126
236
  Claude Code Remote Pilot
127
237
 
128
238
  Usage:
129
- claude-remote-pilot [--session <name>]
239
+ claude-remote-pilot
130
240
 
131
- Options:
132
- --session <name> Name for this Claude session (defaults to current directory name)
241
+ Interactive commands:
242
+ ${HELP}`);
243
+ process.exit(0);
244
+ }
133
245
 
134
- Environment variables:
135
- TELEGRAM_BOT_TOKEN
136
- TELEGRAM_CHAT_ID
137
- CLAUDE_COMMAND
246
+ const setupRl = readline.createInterface({ input: process.stdin, output: process.stdout });
138
247
 
139
- Example:
140
- claude-remote-pilot --session my-project
141
- TELEGRAM_BOT_TOKEN=xxx TELEGRAM_CHAT_ID=123456 claude-remote-pilot --session api
248
+ await ensureDep(setupRl, 'tmux', 'tmux', tmuxInstallCmd());
249
+ await ensureDep(setupRl, 'claude', 'Claude Code CLI', 'npm install -g @anthropic-ai/claude-code');
250
+ const telegram = await setupTelegram(setupRl);
142
251
 
143
- Attach to session:
144
- tmux attach -t <name>
145
- `);
146
- process.exit(0);
147
- }
252
+ const manager = new SessionManager({ telegram });
148
253
 
149
- // Parse --session flag, fall back to current directory name
150
- const sessionFlagIndex = process.argv.indexOf('--session');
151
- const sessionName = sessionFlagIndex !== -1
152
- ? process.argv[sessionFlagIndex + 1]
153
- : path.basename(process.cwd());
254
+ const cwd = process.cwd();
255
+ const defaultName = path.basename(cwd);
256
+ const mount = await question(setupRl, `Mount current directory as a session? (${defaultName}) [y/n] `);
154
257
 
155
- if (!sessionName || sessionName.startsWith('--')) {
156
- console.error('Error: --session requires a name, e.g. --session my-project');
157
- process.exit(1);
158
- }
258
+ if (mount === 'y' || mount === 'yes') {
259
+ const rawName = await questionRaw(setupRl, `Session name [${defaultName}]: `);
260
+ const session = manager.spawn(cwd, rawName || defaultName);
261
+ console.log(` ✓ "${session.name}" started at ${session.path}`);
262
+ console.log(` tmux attach -t ${session.name}\n`);
263
+ }
159
264
 
160
- process.env.CLAUDE_SESSION = sessionName;
161
- process.env.CLAUDE_WORKDIR = process.cwd();
265
+ setupRl.close();
266
+
267
+ console.log(' Type help for commands.\n');
268
+ startREPL(manager);
269
+ }
162
270
 
163
- main();
271
+ main().catch(err => { console.error(err.message); process.exit(1); });
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+ const { execSync } = require('child_process');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const Watcher = require('./Watcher');
6
+
7
+ function sanitizeName(name) {
8
+ return name.replace(/[.:\s]/g, '-');
9
+ }
10
+
11
+ class SessionManager {
12
+ constructor({ telegram = {} } = {}) {
13
+ this.sessions = new Map();
14
+ this.telegram = telegram;
15
+ }
16
+
17
+ spawn(dirPath, name) {
18
+ const resolved = path.resolve(dirPath.replace(/^~/, process.env.HOME || ''));
19
+ if (!fs.existsSync(resolved)) throw new Error(`Path not found: ${resolved}`);
20
+
21
+ const sessionName = sanitizeName(name || path.basename(resolved));
22
+ if (this.sessions.has(sessionName)) throw new Error(`Session "${sessionName}" already exists.`);
23
+
24
+ // Kill stale tmux session from a previous crashed run
25
+ try { execSync(`tmux has-session -t "${sessionName}"`, { stdio: 'ignore' });
26
+ execSync(`tmux kill-session -t "${sessionName}"`, { stdio: 'ignore' });
27
+ } catch {}
28
+
29
+ execSync(`tmux new-session -d -s "${sessionName}" -c "${resolved}" "claude"`, { stdio: 'ignore' });
30
+
31
+ const session = { name: sessionName, path: resolved, status: 'running', startedAt: new Date(), resumeAt: null };
32
+
33
+ const watcher = new Watcher(session, {
34
+ telegram: this.telegram,
35
+ onEnded: (s) => this.sessions.delete(s.name),
36
+ });
37
+ watcher.start();
38
+
39
+ this.sessions.set(sessionName, { session, watcher });
40
+ return session;
41
+ }
42
+
43
+ kill(name) {
44
+ const entry = this.sessions.get(name);
45
+ if (!entry) throw new Error(`Session "${name}" not found.`);
46
+ entry.watcher.stop();
47
+ try { execSync(`tmux kill-session -t "${name}"`, { stdio: 'ignore' }); } catch {}
48
+ this.sessions.delete(name);
49
+ }
50
+
51
+ list() {
52
+ return [...this.sessions.values()].map(e => e.session);
53
+ }
54
+ }
55
+
56
+ module.exports = SessionManager;
package/lib/Watcher.js ADDED
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+ const { execSync } = require('child_process');
3
+ const crypto = require('crypto');
4
+ const notifier = require('./notifier');
5
+
6
+ const LIMIT_RE = /hit your limit|usage limit|rate limit|limit reached|try again|resets/i;
7
+ const PERM_RE = /permission|do you want to proceed|approve/i;
8
+
9
+ class Watcher {
10
+ constructor(session, opts = {}) {
11
+ this.session = session;
12
+ this.telegram = opts.telegram || {};
13
+ this.onEnded = opts.onEnded || (() => {});
14
+ this.checkInterval = opts.checkInterval || 30000;
15
+ this.fallbackWait = opts.fallbackWait || 300;
16
+ this.cooldown = opts.cooldown || 180;
17
+ this.captureLines = opts.captureLines || 500;
18
+ this.lastHash = '';
19
+ this.lastResumeAt = 0;
20
+ this._timer = null;
21
+ this._busy = false;
22
+ }
23
+
24
+ start() {
25
+ this._timer = setInterval(() => this._check(), this.checkInterval);
26
+ }
27
+
28
+ stop() {
29
+ if (this._timer) clearInterval(this._timer);
30
+ this._timer = null;
31
+ }
32
+
33
+ _capture() {
34
+ try {
35
+ return execSync(
36
+ `tmux capture-pane -pt "${this.session.name}" -S "-${this.captureLines}"`,
37
+ { encoding: 'utf8' }
38
+ );
39
+ } catch { return ''; }
40
+ }
41
+
42
+ _hash(text) {
43
+ return crypto.createHash('sha256').update(text.slice(-2000)).digest('hex');
44
+ }
45
+
46
+ _parseWait(text) {
47
+ const m = text.match(/(?:try again|retry|wait).*?in\s+(\d+)\s*(second|minute|hour)/i);
48
+ if (m) {
49
+ const v = parseInt(m[1]);
50
+ if (m[2].startsWith('second')) return Math.max(10, v);
51
+ if (m[2].startsWith('minute')) return Math.max(10, v * 60);
52
+ return Math.max(10, v * 3600);
53
+ }
54
+ return this.fallbackWait;
55
+ }
56
+
57
+ async _check() {
58
+ if (this._busy) return;
59
+ this._busy = true;
60
+ try {
61
+ try { execSync(`tmux has-session -t "${this.session.name}"`, { stdio: 'ignore' }); }
62
+ catch {
63
+ this.stop();
64
+ this.onEnded(this.session);
65
+ notifier.send(this.telegram.token, this.telegram.chatId,
66
+ `Pilot: session "${this.session.name}" ended.`);
67
+ return;
68
+ }
69
+
70
+ const text = this._capture();
71
+
72
+ if (LIMIT_RE.test(text)) {
73
+ await this._handleLimit(text);
74
+ } else if (PERM_RE.test(text)) {
75
+ if (this.session.status !== 'needs-input') {
76
+ this.session.status = 'needs-input';
77
+ notifier.send(this.telegram.token, this.telegram.chatId,
78
+ `Pilot: "${this.session.name}" needs input.`);
79
+ }
80
+ } else if (this.session.status !== 'running') {
81
+ this.session.status = 'running';
82
+ this.session.resumeAt = null;
83
+ }
84
+ } finally {
85
+ this._busy = false;
86
+ }
87
+ }
88
+
89
+ async _handleLimit(text) {
90
+ const hash = this._hash(text);
91
+ if (hash === this.lastHash) return;
92
+ if ((Date.now() / 1000) - this.lastResumeAt < this.cooldown) return;
93
+
94
+ this.lastHash = hash;
95
+ const wait = this._parseWait(text);
96
+ this.session.status = 'limit';
97
+ this.session.resumeAt = Date.now() + wait * 1000;
98
+
99
+ notifier.send(this.telegram.token, this.telegram.chatId,
100
+ `Pilot: limit in "${this.session.name}". Waiting ${wait}s before resume.`);
101
+
102
+ await new Promise(r => setTimeout(r, wait * 1000));
103
+
104
+ try { execSync(`tmux send-keys -t "${this.session.name}" "continue" Enter`, { stdio: 'ignore' }); }
105
+ catch {}
106
+
107
+ this.lastResumeAt = Date.now() / 1000;
108
+ this.session.status = 'running';
109
+ this.session.resumeAt = null;
110
+
111
+ notifier.send(this.telegram.token, this.telegram.chatId,
112
+ `Pilot: resumed "${this.session.name}".`);
113
+ }
114
+ }
115
+
116
+ module.exports = Watcher;
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+ const { execSync } = require('child_process');
3
+
4
+ function send(token, chatId, message) {
5
+ if (!token || !chatId) return;
6
+ try {
7
+ execSync(
8
+ `curl -sS -X POST "https://api.telegram.org/bot${token}/sendMessage"` +
9
+ ` --data-urlencode "chat_id=${chatId}"` +
10
+ ` --data-urlencode "text=${message}"`,
11
+ { stdio: 'ignore', timeout: 5000 }
12
+ );
13
+ } catch {}
14
+ }
15
+
16
+ module.exports = { send };
package/package.json CHANGED
@@ -1,21 +1,19 @@
1
1
  {
2
2
  "name": "claude-code-remote-pilot",
3
- "version": "0.1.5",
4
- "description": "Interactive Claude Code supervisor with tmux, Telegram notifications, and auto-resume after usage limits.",
3
+ "version": "0.2.1",
4
+ "description": "Interactive Claude Code supervisor spawn and monitor multiple Claude sessions from a single terminal.",
5
5
  "type": "commonjs",
6
6
  "bin": {
7
7
  "claude-remote-pilot": "bin/claude-pilot.js"
8
8
  },
9
9
  "files": [
10
10
  "bin/",
11
- "claude-pilot.sh",
11
+ "lib/",
12
12
  "README.md",
13
13
  "LICENSE"
14
14
  ],
15
15
  "scripts": {
16
- "start": "node bin/claude-pilot.js",
17
- "pilot": "node bin/claude-pilot.js",
18
- "check": "node bin/claude-pilot.js --help"
16
+ "start": "node bin/claude-pilot.js"
19
17
  },
20
18
  "keywords": [
21
19
  "claude-code",
@@ -23,7 +21,8 @@
23
21
  "tmux",
24
22
  "telegram",
25
23
  "agent",
26
- "auto-resume"
24
+ "auto-resume",
25
+ "multi-session"
27
26
  ],
28
27
  "author": "mekku",
29
28
  "license": "MIT",
package/claude-pilot.sh DELETED
@@ -1,223 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -u
3
-
4
- # Claude Code Pilot
5
- # Interactive Claude Code supervisor using tmux + Telegram notifications.
6
- #
7
- # Required env:
8
- # TELEGRAM_BOT_TOKEN
9
- # TELEGRAM_CHAT_ID
10
- #
11
- # Optional env:
12
- # CLAUDE_SESSION=claude
13
- # CLAUDE_COMMAND=claude
14
- # CHECK_INTERVAL_SECONDS=30
15
- # LIMIT_FALLBACK_WAIT_SECONDS=300
16
- # POST_RESUME_COOLDOWN_SECONDS=180
17
- # CAPTURE_LINES=500
18
- # RESUME_COMMAND=continue
19
- # START_IF_MISSING=1
20
- #
21
- # Example:
22
- # TELEGRAM_BOT_TOKEN="123:abc" TELEGRAM_CHAT_ID="123456" ./claude-pilot.sh
23
-
24
- CLAUDE_SESSION="${CLAUDE_SESSION:-claude}"
25
- CLAUDE_COMMAND="${CLAUDE_COMMAND:-claude}"
26
- CLAUDE_WORKDIR="${CLAUDE_WORKDIR:-$PWD}"
27
- CHECK_INTERVAL_SECONDS="${CHECK_INTERVAL_SECONDS:-30}"
28
- LIMIT_FALLBACK_WAIT_SECONDS="${LIMIT_FALLBACK_WAIT_SECONDS:-300}"
29
- POST_RESUME_COOLDOWN_SECONDS="${POST_RESUME_COOLDOWN_SECONDS:-180}"
30
- CAPTURE_LINES="${CAPTURE_LINES:-500}"
31
- RESUME_COMMAND="${RESUME_COMMAND:-continue}"
32
- START_IF_MISSING="${START_IF_MISSING:-1}"
33
-
34
- TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"
35
- TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}"
36
-
37
- LIMIT_REGEX="${LIMIT_REGEX:-hit your limit|usage limit|rate limit|limit reached|try again|resets}"
38
- PERMISSION_REGEX="${PERMISSION_REGEX:-permission|do you want to proceed|approve}"
39
-
40
- last_limit_hash=""
41
- last_resume_epoch=0
42
-
43
- log() {
44
- printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
45
- }
46
-
47
- require_command() {
48
- if ! command -v "$1" >/dev/null 2>&1; then
49
- echo "Missing required command: $1" >&2
50
- exit 1
51
- fi
52
- }
53
-
54
- send_telegram() {
55
- local message="$1"
56
-
57
- if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
58
- log "Telegram not configured: $message"
59
- return 0
60
- fi
61
-
62
- curl -sS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
63
- --data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \
64
- --data-urlencode "text=${message}" >/dev/null || true
65
- }
66
-
67
- capture_pane() {
68
- tmux capture-pane -pt "$CLAUDE_SESSION" -S "-${CAPTURE_LINES}" 2>/dev/null || true
69
- }
70
-
71
- hash_text() {
72
- if command -v sha256sum >/dev/null 2>&1; then
73
- sha256sum | awk '{print $1}'
74
- else
75
- shasum -a 256 | awk '{print $1}'
76
- fi
77
- }
78
-
79
- parse_wait_seconds() {
80
- local text="$1"
81
-
82
- if command -v python3 >/dev/null 2>&1; then
83
- TEXT="$text" FALLBACK="$LIMIT_FALLBACK_WAIT_SECONDS" python3 - <<'PY'
84
- import os
85
- import re
86
- from datetime import datetime, timedelta
87
-
88
- text = os.environ.get("TEXT", "")
89
- fallback = int(os.environ.get("FALLBACK", "300"))
90
- now = datetime.now()
91
- lower = text.lower()
92
-
93
- # Examples:
94
- # try again in 12 minutes
95
- # retry after 1 hour
96
- m = re.search(r"(?:try again|retry|wait).*?in\s+(\d+)\s*(second|seconds|minute|minutes|hour|hours)", lower, re.I | re.S)
97
- if m:
98
- value = int(m.group(1))
99
- unit = m.group(2)
100
- if unit.startswith("second"):
101
- print(max(10, value))
102
- elif unit.startswith("minute"):
103
- print(max(10, value * 60))
104
- else:
105
- print(max(10, value * 3600))
106
- raise SystemExit
107
-
108
- # Examples:
109
- # resets 2am
110
- # resets at 14:30
111
- # limit resets at 3:05 pm
112
- m = re.search(r"resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?", lower, re.I)
113
- if m:
114
- hour = int(m.group(1))
115
- minute = int(m.group(2) or 0)
116
- ampm = m.group(3)
117
-
118
- if ampm:
119
- if ampm == "pm" and hour != 12:
120
- hour += 12
121
- if ampm == "am" and hour == 12:
122
- hour = 0
123
-
124
- if 0 <= hour <= 23 and 0 <= minute <= 59:
125
- reset = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
126
- if reset <= now:
127
- reset += timedelta(days=1)
128
- # Add small safety buffer so we do not resume one second too early.
129
- print(max(10, int((reset - now).total_seconds()) + 30))
130
- raise SystemExit
131
-
132
- print(fallback)
133
- PY
134
- return 0
135
- fi
136
-
137
- echo "$LIMIT_FALLBACK_WAIT_SECONDS"
138
- }
139
-
140
- ensure_tmux_session() {
141
- if tmux has-session -t "$CLAUDE_SESSION" 2>/dev/null; then
142
- return 0
143
- fi
144
-
145
- if [ "$START_IF_MISSING" != "1" ]; then
146
- echo "tmux session '$CLAUDE_SESSION' not found." >&2
147
- exit 1
148
- fi
149
-
150
- tmux new-session -d -s "$CLAUDE_SESSION" -c "$CLAUDE_WORKDIR" "$CLAUDE_COMMAND"
151
- log "Started tmux session '$CLAUDE_SESSION' in $CLAUDE_WORKDIR"
152
- send_telegram "Claude Pilot: started tmux session '$CLAUDE_SESSION'."
153
- }
154
-
155
- within_resume_cooldown() {
156
- local now
157
- now=$(date +%s)
158
- [ $((now - last_resume_epoch)) -lt "$POST_RESUME_COOLDOWN_SECONDS" ]
159
- }
160
-
161
- handle_limit() {
162
- local text="$1"
163
- local fingerprint
164
- fingerprint=$(printf '%s' "$text" | tail -n 40 | hash_text)
165
-
166
- if [ "$fingerprint" = "$last_limit_hash" ]; then
167
- return 0
168
- fi
169
-
170
- if within_resume_cooldown; then
171
- return 0
172
- fi
173
-
174
- last_limit_hash="$fingerprint"
175
-
176
- local wait_seconds
177
- wait_seconds=$(parse_wait_seconds "$text")
178
-
179
- log "Limit detected. Waiting ${wait_seconds}s before resume."
180
- send_telegram "Claude Pilot: usage/rate limit detected. Waiting ${wait_seconds}s before sending '${RESUME_COMMAND}'."
181
-
182
- sleep "$wait_seconds"
183
-
184
- tmux send-keys -t "$CLAUDE_SESSION" "$RESUME_COMMAND" Enter
185
- last_resume_epoch=$(date +%s)
186
-
187
- log "Resume command sent."
188
- send_telegram "Claude Pilot: resume command sent to tmux session '$CLAUDE_SESSION'."
189
- }
190
-
191
- main() {
192
- require_command tmux
193
- require_command curl
194
-
195
- ensure_tmux_session
196
-
197
- log "Watching tmux session '$CLAUDE_SESSION'. Attach with: tmux attach -t $CLAUDE_SESSION"
198
- send_telegram "Claude Pilot: watching tmux session '$CLAUDE_SESSION'."
199
-
200
- while true; do
201
- if ! tmux has-session -t "$CLAUDE_SESSION" 2>/dev/null; then
202
- log "tmux session '$CLAUDE_SESSION' ended."
203
- send_telegram "Claude Pilot: tmux session '$CLAUDE_SESSION' ended."
204
- exit 0
205
- fi
206
-
207
- text=$(capture_pane)
208
-
209
- if printf '%s' "$text" | grep -Eiq "$LIMIT_REGEX"; then
210
- handle_limit "$text"
211
- fi
212
-
213
- if printf '%s' "$text" | grep -Eiq "$PERMISSION_REGEX"; then
214
- # Notification only. Do not auto-approve permissions.
215
- send_telegram "Claude Pilot: Claude may need permission/input in session '$CLAUDE_SESSION'."
216
- sleep 60
217
- fi
218
-
219
- sleep "$CHECK_INTERVAL_SECONDS"
220
- done
221
- }
222
-
223
- main "$@"