ghostterm 1.1.2 → 1.2.0
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 +60 -0
- package/README.md +26 -7
- package/bin/ghostterm.js +118 -0
- package/package.json +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.2.0 (2026-03-17)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- **Background mode by default** — `npx ghostterm` now runs in the background automatically, no terminal window
|
|
7
|
+
- **CLI commands** — `npx ghostterm stop`, `npx ghostterm status`, `npx ghostterm logs`
|
|
8
|
+
- **First-run login** — opens browser for Google sign-in in foreground, then auto-restarts as daemon
|
|
9
|
+
- **Duplicate detection** — prevents starting multiple instances
|
|
10
|
+
- **12345 numpad popup** — quick number selection for CLI surveys/prompts (raw keypress without Enter)
|
|
11
|
+
- **Screen button** — renamed from "Shot" for clarity
|
|
12
|
+
- **Paste button shows filename** — after uploading, displays shortened filename instead of generic "Paste"
|
|
13
|
+
- **Session persistence** — refreshing the page restores your last active terminal session
|
|
14
|
+
- **PM2 support** — can run in background without a terminal window
|
|
15
|
+
|
|
16
|
+
### Security & Stability
|
|
17
|
+
- **Heartbeat ping/pong** — relay pings every 20s; mobile detects dead connections within 30s and auto-reconnects
|
|
18
|
+
- **Disconnect overlay** — clear full-screen prompt when companion goes offline
|
|
19
|
+
- **Session loading spinner** — visual feedback when switching sessions
|
|
20
|
+
- **Improved toast notifications** — slide-in animation
|
|
21
|
+
|
|
22
|
+
### Docs
|
|
23
|
+
- Updated README with new features, PM2 instructions, heartbeat details
|
|
24
|
+
- Added CHANGELOG
|
|
25
|
+
|
|
26
|
+
## 1.1.1 (2026-03-16)
|
|
27
|
+
|
|
28
|
+
### Security Hardening
|
|
29
|
+
- **Rate limiting** — 50 messages/second per WebSocket connection
|
|
30
|
+
- **IP connection limit** — max 5 concurrent connections per IP
|
|
31
|
+
- **Brute force protection** — pair code entry locked after 3 failed attempts (60s cooldown)
|
|
32
|
+
- **Origin verification** — only ghostterm.pages.dev allowed
|
|
33
|
+
- **Max payload** — 1MB limit per WebSocket message
|
|
34
|
+
- **Timing-safe HMAC** — prevents timing attacks on token verification
|
|
35
|
+
- **Upload validation** — 5MB size limit + extension whitelist
|
|
36
|
+
- **Pair code format validation** — must be exactly 6 digits
|
|
37
|
+
- **WSS enforcement** — prevents MITM downgrade attacks
|
|
38
|
+
- **Exponential backoff** — reconnection delay 1s → 30s max
|
|
39
|
+
|
|
40
|
+
### Bug Fixes
|
|
41
|
+
- Fixed ghost duplication (stale session list cleared on reconnect)
|
|
42
|
+
- Fixed auto-scroll during thinking output (touch/button scroll disables auto-scroll)
|
|
43
|
+
- Fixed smartScroll jumping to top of terminal
|
|
44
|
+
|
|
45
|
+
## 1.1.0 (2026-03-15)
|
|
46
|
+
|
|
47
|
+
### Features
|
|
48
|
+
- Google OAuth auto-pairing (sign in once, auto-reconnects)
|
|
49
|
+
- Long-lived token (30 days, no repeated Google sign-in)
|
|
50
|
+
- 4 simultaneous terminal sessions with ghost cell previews
|
|
51
|
+
- Pixel office mode
|
|
52
|
+
- File upload and screenshot support
|
|
53
|
+
- D-pad controls, quick keys (y/n/Tab/Shift+Tab)
|
|
54
|
+
- Copy mode for terminal text selection
|
|
55
|
+
|
|
56
|
+
## 1.0.0 (2026-03-14)
|
|
57
|
+
|
|
58
|
+
- Initial release
|
|
59
|
+
- Basic terminal relay via WebSocket
|
|
60
|
+
- Pair code pairing
|
package/README.md
CHANGED
|
@@ -38,7 +38,23 @@ No VPN. No Tailscale. No port forwarding. Just one command.
|
|
|
38
38
|
npx ghostterm
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
First run opens a browser for Google sign-in. After that, it
|
|
41
|
+
First run opens a browser for Google sign-in. After that, it runs in the background automatically — no terminal window.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
$ npx ghostterm
|
|
45
|
+
✅ GhostTerm running in background (PID: 12345)
|
|
46
|
+
📱 Open: ghostterm.pages.dev
|
|
47
|
+
🛑 Stop: npx ghostterm stop
|
|
48
|
+
📋 Logs: npx ghostterm logs
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Other commands:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx ghostterm status # check if running
|
|
55
|
+
npx ghostterm stop # stop the background process
|
|
56
|
+
npx ghostterm logs # show recent logs
|
|
57
|
+
```
|
|
42
58
|
|
|
43
59
|
### 2. On your phone
|
|
44
60
|
|
|
@@ -73,16 +89,18 @@ Open **[ghostterm.pages.dev](https://ghostterm.pages.dev)** in any mobile browse
|
|
|
73
89
|
### Controls
|
|
74
90
|
|
|
75
91
|
- **Quick keys** — one-tap `y`/`n` for Claude Code approvals
|
|
76
|
-
- **
|
|
92
|
+
- **12345 numpad** — tap to pop up number selection for CLI surveys/prompts (raw keypress, no Enter)
|
|
93
|
+
- **D-pad** — arrow keys, Enter, Tab, Shift+Tab, Space
|
|
77
94
|
- **`claude` button** — quick-launch menu: new session, resume, continue, dangerous mode
|
|
78
|
-
- **Ctrl+C** — interrupt running processes
|
|
95
|
+
- **Ctrl+C (Stop)** — interrupt running processes
|
|
79
96
|
- **Text input** — full keyboard input with Send button
|
|
80
97
|
- **Copy mode** — select and copy terminal text
|
|
81
98
|
|
|
82
99
|
### File Transfer
|
|
83
100
|
|
|
84
|
-
- **
|
|
101
|
+
- **Screen** — capture your terminal screen and send it to your PC as a file
|
|
85
102
|
- **File upload** — upload files directly from your phone to your desktop
|
|
103
|
+
- **Paste button** — after uploading, shows shortened filename; tap to paste the file path into the terminal
|
|
86
104
|
|
|
87
105
|
### Visual
|
|
88
106
|
|
|
@@ -95,7 +113,9 @@ Open **[ghostterm.pages.dev](https://ghostterm.pages.dev)** in any mobile browse
|
|
|
95
113
|
- **Google auto-pairing** — sign in once, auto-reconnects forever
|
|
96
114
|
- **Encrypted relay** — all traffic over WSS (WebSocket Secure)
|
|
97
115
|
- **Zero data stored** — the relay only forwards messages
|
|
98
|
-
- **
|
|
116
|
+
- **Heartbeat** — relay pings every 20s; dead connections detected and auto-reconnected within 30s
|
|
117
|
+
- **Auto-reconnect** — exponential backoff (1s → 30s max), handles network drops gracefully
|
|
118
|
+
- **Session persistence** — refreshing the page restores your last active terminal session
|
|
99
119
|
|
|
100
120
|
---
|
|
101
121
|
|
|
@@ -175,8 +195,7 @@ GHOSTTERM_RELAY=wss://your-relay.example.com npx ghostterm
|
|
|
175
195
|
|
|
176
196
|
## Pricing
|
|
177
197
|
|
|
178
|
-
|
|
179
|
-
- **Pro**: $5/month or $12/quarter — unlimited access
|
|
198
|
+
GhostTerm is currently **free** during early access. Pro plans coming soon.
|
|
180
199
|
|
|
181
200
|
---
|
|
182
201
|
|
package/bin/ghostterm.js
CHANGED
|
@@ -433,8 +433,106 @@ function handleRelayMessage(msg) {
|
|
|
433
433
|
}
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
// ==================== Background Mode ====================
|
|
437
|
+
const PID_FILE = path.join(CRED_DIR, 'ghostterm.pid');
|
|
438
|
+
const LOG_FILE = path.join(CRED_DIR, 'ghostterm.log');
|
|
439
|
+
|
|
440
|
+
function isRunning(pid) {
|
|
441
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function handleSubcommand() {
|
|
445
|
+
const arg = process.argv[2];
|
|
446
|
+
|
|
447
|
+
if (arg === 'stop') {
|
|
448
|
+
if (fs.existsSync(PID_FILE)) {
|
|
449
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8'));
|
|
450
|
+
if (isRunning(pid)) {
|
|
451
|
+
process.kill(pid);
|
|
452
|
+
fs.unlinkSync(PID_FILE);
|
|
453
|
+
console.log(` GhostTerm stopped (PID: ${pid})`);
|
|
454
|
+
} else {
|
|
455
|
+
fs.unlinkSync(PID_FILE);
|
|
456
|
+
console.log(' GhostTerm was not running');
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
console.log(' GhostTerm is not running');
|
|
460
|
+
}
|
|
461
|
+
process.exit(0);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (arg === 'status') {
|
|
465
|
+
if (fs.existsSync(PID_FILE)) {
|
|
466
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8'));
|
|
467
|
+
if (isRunning(pid)) {
|
|
468
|
+
console.log(` GhostTerm is running (PID: ${pid})`);
|
|
469
|
+
} else {
|
|
470
|
+
fs.unlinkSync(PID_FILE);
|
|
471
|
+
console.log(' GhostTerm is not running');
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
console.log(' GhostTerm is not running');
|
|
475
|
+
}
|
|
476
|
+
process.exit(0);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (arg === 'logs') {
|
|
480
|
+
if (fs.existsSync(LOG_FILE)) {
|
|
481
|
+
const lines = fs.readFileSync(LOG_FILE, 'utf8').split('\n').slice(-50);
|
|
482
|
+
console.log(lines.join('\n'));
|
|
483
|
+
} else {
|
|
484
|
+
console.log(' No logs found');
|
|
485
|
+
}
|
|
486
|
+
process.exit(0);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// No subcommand = start in background (unless already daemonized via --daemon)
|
|
490
|
+
if (arg !== '--daemon') {
|
|
491
|
+
// Check if already running
|
|
492
|
+
if (fs.existsSync(PID_FILE)) {
|
|
493
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8'));
|
|
494
|
+
if (isRunning(pid)) {
|
|
495
|
+
console.log(` GhostTerm is already running (PID: ${pid})`);
|
|
496
|
+
console.log(' Stop: npx ghostterm stop');
|
|
497
|
+
process.exit(0);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// First-time login needs foreground (browser opens)
|
|
502
|
+
const needsLogin = !fs.existsSync(CRED_FILE);
|
|
503
|
+
if (needsLogin) {
|
|
504
|
+
// Run in foreground for first login, then restart as daemon
|
|
505
|
+
return 'foreground-first-login';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Spawn self as daemon
|
|
509
|
+
const { spawn } = require('child_process');
|
|
510
|
+
const out = fs.openSync(LOG_FILE, 'a');
|
|
511
|
+
const child = spawn(process.execPath, [__filename, '--daemon'], {
|
|
512
|
+
detached: true,
|
|
513
|
+
stdio: ['ignore', out, out],
|
|
514
|
+
env: { ...process.env },
|
|
515
|
+
});
|
|
516
|
+
child.unref();
|
|
517
|
+
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
518
|
+
console.log('');
|
|
519
|
+
console.log(` ✅ GhostTerm running in background (PID: ${child.pid})`);
|
|
520
|
+
console.log(' 📱 Open: ghostterm.pages.dev');
|
|
521
|
+
console.log(' 🛑 Stop: npx ghostterm stop');
|
|
522
|
+
console.log(' 📋 Logs: npx ghostterm logs');
|
|
523
|
+
console.log('');
|
|
524
|
+
process.exit(0);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// --daemon: write PID file and continue to main()
|
|
528
|
+
fs.writeFileSync(PID_FILE, String(process.pid));
|
|
529
|
+
return 'daemon';
|
|
530
|
+
}
|
|
531
|
+
|
|
436
532
|
// ==================== Main ====================
|
|
437
533
|
async function main() {
|
|
534
|
+
const mode = handleSubcommand();
|
|
535
|
+
|
|
438
536
|
console.log('');
|
|
439
537
|
console.log(' ╔═══════════════════════════════════╗');
|
|
440
538
|
console.log(' ║ GhostTerm Companion ║');
|
|
@@ -449,6 +547,26 @@ async function main() {
|
|
|
449
547
|
console.log(' Will use pairing code instead');
|
|
450
548
|
}
|
|
451
549
|
|
|
550
|
+
// First login done in foreground — now restart as daemon
|
|
551
|
+
if (mode === 'foreground-first-login' && googleToken) {
|
|
552
|
+
console.log('');
|
|
553
|
+
console.log(' Login successful! Restarting in background...');
|
|
554
|
+
const { spawn } = require('child_process');
|
|
555
|
+
const out = fs.openSync(LOG_FILE, 'a');
|
|
556
|
+
const child = spawn(process.execPath, [__filename, '--daemon'], {
|
|
557
|
+
detached: true,
|
|
558
|
+
stdio: ['ignore', out, out],
|
|
559
|
+
env: { ...process.env },
|
|
560
|
+
});
|
|
561
|
+
child.unref();
|
|
562
|
+
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
563
|
+
console.log(` ✅ GhostTerm running in background (PID: ${child.pid})`);
|
|
564
|
+
console.log(' 📱 Open: ghostterm.pages.dev');
|
|
565
|
+
console.log(' 🛑 Stop: npx ghostterm stop');
|
|
566
|
+
console.log('');
|
|
567
|
+
process.exit(0);
|
|
568
|
+
}
|
|
569
|
+
|
|
452
570
|
connectToRelay();
|
|
453
571
|
}
|
|
454
572
|
|