claude-code-remote-pilot 0.4.3 → 0.4.5
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/CHANGELOG.md +16 -0
- package/bin/claude-pilot.js +17 -3
- package/lib/WebServer.js +30 -3
- package/lib/ui.html +412 -421
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.5 — 2026-05-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Terminal redesign**: input is now embedded inside the terminal box (dark background, monospace font, `❯` prompt) — feels like a real terminal. After sending, focus returns to the input automatically.
|
|
7
|
+
- **Full-height terminal**: terminal fills the viewport height (`calc(100vh - 210px)`) with the output scrolling above the pinned input row.
|
|
8
|
+
- **Password auth**: `web [port] [host] [password]` — if a password is given, the browser shows a login screen. Token is stored in localStorage and sent as `Authorization: Bearer` on all requests (query param for SSE). `POST /api/login` is the only unauthenticated endpoint.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 0.4.4 — 2026-05-06
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Press `w` in watch mode to open (or reuse) the web dashboard in the browser. Starts the server on `127.0.0.1:3742` if not already running. Shown in the watch footer: `w: web ui`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
3
19
|
## 0.4.3 — 2026-05-06
|
|
4
20
|
|
|
5
21
|
### Added
|
package/bin/claude-pilot.js
CHANGED
|
@@ -170,7 +170,7 @@ function renderWatchTable(allSessions, selectedIdx) {
|
|
|
170
170
|
footer = ` [t] terminal [k] kill Esc: back`;
|
|
171
171
|
}
|
|
172
172
|
} else {
|
|
173
|
-
footer = ` [1-${Math.min(allSessions.length, 9)}]: select session q: exit watch`;
|
|
173
|
+
footer = ` [1-${Math.min(allSessions.length, 9)}]: select session w: web ui q: exit watch`;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
return ['\n', ' Claude Code Remote Pilot', bar, header, bar, ...rows, bar, footer, ''].join('\n');
|
|
@@ -222,6 +222,18 @@ function startWatch(manager, rl) {
|
|
|
222
222
|
|
|
223
223
|
if (selectedIdx < 0) {
|
|
224
224
|
if (str === 'q' || str === 'Q') { exitWatch(); return; }
|
|
225
|
+
if (str === 'w' || str === 'W') {
|
|
226
|
+
let webServer = manager._webServer;
|
|
227
|
+
if (!webServer) {
|
|
228
|
+
webServer = new WebServer(manager, 3742, '127.0.0.1');
|
|
229
|
+
manager._webServer = webServer;
|
|
230
|
+
webServer.start();
|
|
231
|
+
}
|
|
232
|
+
const url = `http://${webServer.host}:${webServer.port}`;
|
|
233
|
+
const opener = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
234
|
+
spawn(opener, [url], { stdio: 'ignore', detached: true }).unref();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
225
237
|
const n = parseInt(str);
|
|
226
238
|
if (!isNaN(n) && n >= 1 && n <= Math.min(allSessions.length, 9)) {
|
|
227
239
|
selectedIdx = n - 1;
|
|
@@ -319,7 +331,7 @@ const HELP = `
|
|
|
319
331
|
spawn <path> [name] Start Claude at path (name defaults to dir name)
|
|
320
332
|
list Show all sessions
|
|
321
333
|
watch Live session monitor (q to exit)
|
|
322
|
-
web [port] [host]
|
|
334
|
+
web [port] [host] [password] Start web dashboard (default: 3742 127.0.0.1)
|
|
323
335
|
attach <name> Open tmux session in this terminal
|
|
324
336
|
kill <name> Stop a session
|
|
325
337
|
resume [message] Show or set the message sent after a limit resets
|
|
@@ -433,16 +445,18 @@ ${HELP}`);
|
|
|
433
445
|
case 'web': {
|
|
434
446
|
const port = parseInt(args[0]) || 3742;
|
|
435
447
|
const host = args[1] || '127.0.0.1';
|
|
448
|
+
const password = args[2] || null;
|
|
436
449
|
let webServer = manager._webServer;
|
|
437
450
|
if (webServer) {
|
|
438
451
|
console.log(` Web dashboard already running at http://${webServer.host}:${webServer.port}`);
|
|
439
452
|
break;
|
|
440
453
|
}
|
|
441
|
-
webServer = new WebServer(manager, port, host);
|
|
454
|
+
webServer = new WebServer(manager, port, host, password);
|
|
442
455
|
manager._webServer = webServer;
|
|
443
456
|
webServer.start();
|
|
444
457
|
const url = `http://${host}:${port}`;
|
|
445
458
|
console.log(` ✓ Web dashboard started at ${url}`);
|
|
459
|
+
if (password) console.log(` Password protected. Enter password in the browser.`);
|
|
446
460
|
const opener = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
447
461
|
spawn(opener, [url], { stdio: 'ignore', detached: true }).unref();
|
|
448
462
|
break;
|
package/lib/WebServer.js
CHANGED
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
const http = require('http');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
5
6
|
const { spawnSync } = require('child_process');
|
|
6
7
|
const config = require('./config');
|
|
7
8
|
|
|
8
9
|
const STRIP_ANSI = /\x1b\[[0-9;]*[mGKHFABCDJsuhl]|\x1b[()][AB012]/g;
|
|
9
10
|
|
|
10
11
|
class WebServer {
|
|
11
|
-
constructor(manager, port = 3742, host = '127.0.0.1') {
|
|
12
|
+
constructor(manager, port = 3742, host = '127.0.0.1', password = null) {
|
|
12
13
|
this.manager = manager;
|
|
13
14
|
this.port = port;
|
|
14
15
|
this.host = host;
|
|
16
|
+
this.password = password || null;
|
|
17
|
+
this._token = password ? crypto.randomBytes(20).toString('hex') : null;
|
|
15
18
|
this.startedAt = new Date();
|
|
16
19
|
this.server = null;
|
|
17
20
|
this._clients = new Set();
|
|
@@ -47,7 +50,30 @@ class WebServer {
|
|
|
47
50
|
});
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
// Returns true if authorized (or no password set). Sends 401 and returns false otherwise.
|
|
54
|
+
_checkAuth(req, res, url) {
|
|
55
|
+
if (!this.password) return true;
|
|
56
|
+
const authHeader = req.headers['authorization'];
|
|
57
|
+
if (authHeader === `Bearer ${this._token}`) return true;
|
|
58
|
+
if (url && url.searchParams.get('token') === this._token) return true;
|
|
59
|
+
this._json(res, 401, { error: 'Unauthorized' });
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_handleApi(req, res, pathname, url) {
|
|
64
|
+
// POST /api/login — no auth required
|
|
65
|
+
if (req.method === 'POST' && pathname === '/api/login') {
|
|
66
|
+
return this._readBody(req, (err, body) => {
|
|
67
|
+
if (err) return this._json(res, 400, { error: err.message });
|
|
68
|
+
if (!this.password) return this._json(res, 200, { token: null });
|
|
69
|
+
if (body.password !== this.password) return this._json(res, 401, { error: 'Wrong password' });
|
|
70
|
+
return this._json(res, 200, { token: this._token });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// All other API routes require auth
|
|
75
|
+
if (!this._checkAuth(req, res, url)) return;
|
|
76
|
+
|
|
51
77
|
// GET /api/sessions
|
|
52
78
|
if (req.method === 'GET' && pathname === '/api/sessions') {
|
|
53
79
|
return this._json(res, 200, this._buildAllSessions());
|
|
@@ -141,6 +167,7 @@ class WebServer {
|
|
|
141
167
|
}
|
|
142
168
|
|
|
143
169
|
if (pathname === '/events') {
|
|
170
|
+
if (!this._checkAuth(req, res, url)) return;
|
|
144
171
|
res.writeHead(200, {
|
|
145
172
|
'Content-Type': 'text/event-stream',
|
|
146
173
|
'Cache-Control': 'no-cache',
|
|
@@ -153,7 +180,7 @@ class WebServer {
|
|
|
153
180
|
}
|
|
154
181
|
|
|
155
182
|
if (pathname.startsWith('/api/')) {
|
|
156
|
-
return this._handleApi(req, res, pathname);
|
|
183
|
+
return this._handleApi(req, res, pathname, url);
|
|
157
184
|
}
|
|
158
185
|
|
|
159
186
|
res.writeHead(404);
|