ghostterm 2.3.0 → 2.3.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
@@ -68,7 +68,7 @@ ghostterm
68
68
 
69
69
  1. First time: browser opens for Google sign-in (one-time, remembered after)
70
70
  2. Open **[ghostterm.pages.dev](https://ghostterm.pages.dev)** on your phone
71
- 3. Sign in with the same Google account → **auto-connects, no pairing codes**
71
+ 3. Sign in with the same Google account → **auto-connects** or enter the 10-digit pairing code
72
72
 
73
73
  > Don't want to install? Use `npx ghostterm` to run without installing.
74
74
 
@@ -1,288 +1,312 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- // Prevent crash from node-pty conpty_console_list_agent on Windows background processes
5
- process.on('uncaughtException', (err) => {
6
- if (err.message && err.message.includes('AttachConsole')) {
7
- // Ignore — this is a known node-pty issue in background mode
8
- return;
9
- }
10
- console.error('Uncaught error:', err.message);
11
- });
12
-
13
- const WebSocket = require('ws');
14
- const { CompanionWebRTC } = require('../lib/webrtc-peer');
15
- const { PtyManager } = require('../lib/pty-manager');
16
- const { getToken, saveToken } = require('../lib/auth');
17
-
18
- // --- Configuration ---
19
- const DEFAULT_SIGNALING = 'wss://ghostterm-p2p-signal.fly.dev';
20
- const SITE_URL = 'https://ghostterm.pages.dev';
21
-
22
- const args = process.argv.slice(2);
23
- const signalingUrl = getArg('--signal', '-s') || DEFAULT_SIGNALING;
24
- const siteUrl = getArg('--site') || SITE_URL;
25
- const helpRequested = args.includes('--help') || args.includes('-h');
26
-
27
- function getArg(long, short) {
28
- for (let i = 0; i < args.length; i++) {
29
- if (args[i] === long || (short && args[i] === short)) {
30
- return args[i + 1];
31
- }
32
- }
33
- return null;
34
- }
35
-
36
- if (helpRequested) {
37
- console.log(`
38
- GhostTerm P2P - Control your terminal from your phone via WebRTC
39
-
40
- Usage:
41
- npx ghostterm-p2p [options]
42
-
43
- Options:
44
- -s, --signal <url> Signaling server URL (default: ${DEFAULT_SIGNALING})
45
- --site <url> Mobile site URL (default: ${SITE_URL})
46
- -h, --help Show this help
47
-
48
- How it works:
49
- 1. This tool connects to the signaling server and gets a 6-digit pair code
50
- 2. Open the mobile site on your phone and enter the code
51
- 3. A direct P2P WebRTC connection is established
52
- 4. All terminal data flows directly between your phone and PC (encrypted)
53
- 5. The signaling server never sees your terminal data
54
- `);
55
- process.exit(0);
56
- }
57
-
58
- // --- State ---
59
- let ws = null;
60
- let webrtc = null;
61
- let ptyManager = null;
62
- let reconnecting = false;
63
- let authToken = null;
64
-
65
- // --- Main ---
66
-
67
- async function start() {
68
- console.log('\n GhostTerm P2P');
69
- console.log(' =============');
70
-
71
- // Try to get auth token (optional — won't block if no login)
72
- try {
73
- authToken = await getToken();
74
- } catch (e) {
75
- console.log(' Skipping login (anonymous mode)');
76
- authToken = null;
77
- }
78
-
79
- console.log(' Connecting to signaling server...');
80
- connectSignaling();
81
- }
82
-
83
- function connectSignaling() {
84
- const url = `${signalingUrl}?role=companion`;
85
- ws = new WebSocket(url);
86
-
87
- ws.on('open', () => {
88
- console.log(' Connected to signaling server.');
89
- reconnecting = false;
90
- resetReconnectDelay();
91
- // Send auth token if available
92
- if (authToken) {
93
- ws.send(JSON.stringify({ type: 'auth', token: authToken }));
94
- }
95
- });
96
-
97
- ws.on('message', (raw) => {
98
- let msg;
99
- try {
100
- msg = JSON.parse(raw);
101
- } catch {
102
- return;
103
- }
104
- handleSignalingMessage(msg);
105
- });
106
-
107
- ws.on('close', () => {
108
- if (!reconnecting) {
109
- console.log('\n Signaling server disconnected.');
110
- }
111
- scheduleReconnect();
112
- });
113
-
114
- ws.on('error', (err) => {
115
- if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
116
- console.error(` Cannot reach signaling server at ${signalingUrl}`);
117
- }
118
- });
119
- }
120
-
121
- function handleSignalingMessage(msg) {
122
- switch (msg.type) {
123
- case 'waiting_auth':
124
- // Server ready, waiting for Google auth
125
- break;
126
-
127
- case 'auth_ok':
128
- console.log(` Authenticated as: ${msg.email}`);
129
- console.log(`\n Open ${siteUrl} on your phone`);
130
- console.log(' Sign in with the same Google account to connect.\n');
131
- if (msg.longToken) {
132
- saveToken(null, msg.longToken);
133
- authToken = msg.longToken;
134
- }
135
- if (msg.autoPaired) {
136
- console.log(' Mobile already connected! Establishing P2P...');
137
- }
138
- break;
139
-
140
- case 'mobile_connected':
141
- console.log('\n Mobile device connected! Establishing P2P...');
142
- initiateWebRTC();
143
- break;
144
-
145
- case 'webrtc_answer':
146
- case 'webrtc_ice':
147
- if (webrtc) {
148
- webrtc.handleSignaling(msg);
149
- }
150
- break;
151
-
152
- case 'mobile_disconnected':
153
- if (webrtc && webrtc.isActive) break;
154
- console.log('\n Mobile disconnected.');
155
- cleanupWebRTC();
156
- break;
157
-
158
- case 'auth_error':
159
- console.error(` Auth failed: ${msg.message}`);
160
- console.error(' Try deleting ~/.ghostterm/credentials.json and restart.');
161
- break;
162
-
163
- case 'error':
164
- console.error(` Server error: ${msg.message}`);
165
- break;
166
- }
167
- }
168
-
169
- function initiateWebRTC() {
170
- cleanupWebRTC();
171
-
172
- webrtc = new CompanionWebRTC();
173
- ptyManager = new PtyManager();
174
-
175
- // Wire PTY output to WebRTC
176
- ptyManager.on('send', (msg) => {
177
- if (webrtc && webrtc.isActive) {
178
- webrtc.send(msg);
179
- }
180
- });
181
-
182
- // Wire WebRTC messages to PTY
183
- webrtc.on('message', (msg) => {
184
- if (msg.type === 'shutdown') {
185
- console.log('\n Shutdown requested from mobile. Bye!');
186
- cleanupWebRTC();
187
- if (ws) ws.close();
188
- process.exit(0);
189
- }
190
- if (msg.type === 'resize') {
191
- console.log(` [resize] cols=${msg.cols} rows=${msg.rows} sid=${msg.sessionId || 'attached'}`);
192
- }
193
- if (ptyManager) {
194
- const responses = ptyManager.handleMessage(msg);
195
- for (const resp of responses) {
196
- webrtc.send(resp);
197
- }
198
- }
199
- });
200
-
201
- webrtc.on('open', () => {
202
- console.log(' P2P connection established! Terminal is now accessible from your phone.');
203
- console.log(' All data flows directly between your devices (encrypted).');
204
- console.log(' Press Ctrl+C to stop.\n');
205
- });
206
-
207
- webrtc.on('close', () => {
208
- console.log('\n P2P connection closed.');
209
- cleanupPty();
210
- scheduleReconnect();
211
- });
212
-
213
- webrtc.on('timeout', () => {
214
- console.log('\n P2P heartbeat timeout. Connection lost.');
215
- });
216
-
217
- webrtc.on('error', (err) => {
218
- console.error(` WebRTC error: ${err}`);
219
- });
220
-
221
- // Initiate the connection (sends offer via signaling)
222
- webrtc.initiate((msg) => {
223
- if (ws && ws.readyState === WebSocket.OPEN) {
224
- ws.send(JSON.stringify(msg));
225
- }
226
- });
227
- }
228
-
229
- function cleanupWebRTC() {
230
- if (webrtc) {
231
- webrtc.removeAllListeners();
232
- webrtc.close();
233
- webrtc = null;
234
- }
235
- cleanupPty();
236
- }
237
-
238
- function cleanupPty() {
239
- if (ptyManager) {
240
- ptyManager.removeAllListeners();
241
- ptyManager.destroy();
242
- ptyManager = null;
243
- }
244
- }
245
-
246
- let reconnectTimer = null;
247
- let reconnectDelay = 1000; // start at 1s
248
- const RECONNECT_MAX = 30000; // cap at 30s
249
-
250
- function scheduleReconnect() {
251
- if (reconnectTimer) return;
252
- reconnecting = true;
253
- const delay = reconnectDelay;
254
- console.log(` Reconnecting in ${(delay / 1000).toFixed(0)}s...`);
255
- reconnectTimer = setTimeout(() => {
256
- reconnectTimer = null;
257
- connectSignaling();
258
- }, delay);
259
- // Exponential backoff: 1s → 2s → 4s → 8s → 16s → 30s (capped)
260
- reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX);
261
- }
262
-
263
- function resetReconnectDelay() {
264
- reconnectDelay = 1000;
265
- }
266
-
267
- // --- Graceful shutdown ---
268
-
269
- process.on('SIGINT', () => {
270
- console.log('\n Shutting down...');
271
- cleanupWebRTC();
272
- if (ws) {
273
- ws.close();
274
- }
275
- if (reconnectTimer) {
276
- clearTimeout(reconnectTimer);
277
- }
278
- process.exit(0);
279
- });
280
-
281
- process.on('SIGTERM', () => {
282
- cleanupWebRTC();
283
- if (ws) ws.close();
284
- process.exit(0);
285
- });
286
-
287
- // --- Go ---
288
- start();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Prevent crash from node-pty conpty_console_list_agent on Windows background processes
5
+ process.on('uncaughtException', (err) => {
6
+ if (err.message && err.message.includes('AttachConsole')) {
7
+ // Ignore — this is a known node-pty issue in background mode
8
+ return;
9
+ }
10
+ console.error('Uncaught error:', err.message);
11
+ });
12
+
13
+ const WebSocket = require('ws');
14
+ const qrcode = require('qrcode-terminal');
15
+ const { CompanionWebRTC } = require('../lib/webrtc-peer');
16
+ const { PtyManager } = require('../lib/pty-manager');
17
+ const { getToken, saveToken } = require('../lib/auth');
18
+
19
+ // --- Configuration ---
20
+ const DEFAULT_SIGNALING = 'wss://ghostterm-p2p-signal.fly.dev';
21
+ const SITE_URL = 'https://ghostterm.pages.dev';
22
+
23
+ const args = process.argv.slice(2);
24
+ const signalingUrl = getArg('--signal', '-s') || DEFAULT_SIGNALING;
25
+ const siteUrl = getArg('--site') || SITE_URL;
26
+ const helpRequested = args.includes('--help') || args.includes('-h');
27
+
28
+ function getArg(long, short) {
29
+ for (let i = 0; i < args.length; i++) {
30
+ if (args[i] === long || (short && args[i] === short)) {
31
+ return args[i + 1];
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+
37
+ if (helpRequested) {
38
+ console.log(`
39
+ GhostTerm P2P - Control your terminal from your phone via WebRTC
40
+
41
+ Usage:
42
+ npx ghostterm-p2p [options]
43
+
44
+ Options:
45
+ -s, --signal <url> Signaling server URL (default: ${DEFAULT_SIGNALING})
46
+ --site <url> Mobile site URL (default: ${SITE_URL})
47
+ -h, --help Show this help
48
+
49
+ How it works:
50
+ 1. This tool connects to the signaling server and gets a 6-digit pair code
51
+ 2. Open the mobile site on your phone and enter the code
52
+ 3. A direct P2P WebRTC connection is established
53
+ 4. All terminal data flows directly between your phone and PC (encrypted)
54
+ 5. The signaling server never sees your terminal data
55
+ `);
56
+ process.exit(0);
57
+ }
58
+
59
+ // --- State ---
60
+ let ws = null;
61
+ let webrtc = null;
62
+ let ptyManager = null;
63
+ let pairCode = null;
64
+ let reconnecting = false;
65
+ let authToken = null; // Google or long token
66
+
67
+ // --- Main ---
68
+
69
+ async function start() {
70
+ console.log('\n GhostTerm P2P');
71
+ console.log(' =============');
72
+
73
+ // Try to get auth token (optional — won't block if no login)
74
+ try {
75
+ authToken = await getToken();
76
+ } catch (e) {
77
+ console.log(' Skipping login (anonymous mode)');
78
+ authToken = null;
79
+ }
80
+
81
+ console.log(' Connecting to signaling server...');
82
+ connectSignaling();
83
+ }
84
+
85
+ function connectSignaling() {
86
+ const url = `${signalingUrl}?role=companion`;
87
+ ws = new WebSocket(url);
88
+
89
+ ws.on('open', () => {
90
+ console.log(' Connected to signaling server.');
91
+ reconnecting = false;
92
+ resetReconnectDelay();
93
+ // Send auth token if available
94
+ if (authToken) {
95
+ ws.send(JSON.stringify({ type: 'auth', token: authToken }));
96
+ }
97
+ });
98
+
99
+ ws.on('message', (raw) => {
100
+ let msg;
101
+ try {
102
+ msg = JSON.parse(raw);
103
+ } catch {
104
+ return;
105
+ }
106
+ handleSignalingMessage(msg);
107
+ });
108
+
109
+ ws.on('close', () => {
110
+ if (!reconnecting) {
111
+ console.log('\n Signaling server disconnected.');
112
+ }
113
+ scheduleReconnect();
114
+ });
115
+
116
+ ws.on('error', (err) => {
117
+ if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
118
+ console.error(` Cannot reach signaling server at ${signalingUrl}`);
119
+ }
120
+ });
121
+ }
122
+
123
+ function handleSignalingMessage(msg) {
124
+ switch (msg.type) {
125
+ case 'pair_code':
126
+ pairCode = msg.code;
127
+ displayPairCode(msg.code);
128
+ break;
129
+
130
+ case 'mobile_connected':
131
+ console.log('\n Mobile device paired! Establishing P2P connection...');
132
+ initiateWebRTC();
133
+ break;
134
+
135
+ case 'webrtc_answer':
136
+ case 'webrtc_ice':
137
+ if (webrtc) {
138
+ webrtc.handleSignaling(msg);
139
+ }
140
+ break;
141
+
142
+ case 'mobile_disconnected':
143
+ // Ignore if P2P is active — mobile closed signaling WS after P2P established (expected)
144
+ if (webrtc && webrtc.isActive) {
145
+ // P2P still alive, signaling disconnect is normal
146
+ break;
147
+ }
148
+ console.log('\n Mobile disconnected.');
149
+ cleanupWebRTC();
150
+ break;
151
+
152
+ case 'pair_expired':
153
+ console.log('\n Pair code expired. Reconnecting for a new code...');
154
+ ws.close();
155
+ break;
156
+
157
+ case 'auth_ok':
158
+ console.log(` Authenticated as: ${msg.email}`);
159
+ // Save long token for future sessions
160
+ if (msg.longToken) {
161
+ saveToken(null, msg.longToken);
162
+ authToken = msg.longToken;
163
+ }
164
+ break;
165
+
166
+ case 'auth_error':
167
+ console.error(` Auth failed: ${msg.message}`);
168
+ break;
169
+
170
+ case 'error':
171
+ console.error(` Server error: ${msg.message}`);
172
+ break;
173
+ }
174
+ }
175
+
176
+ function displayPairCode(code) {
177
+ const mobileUrl = `${siteUrl}?code=${code}`;
178
+
179
+ console.log('\n +--------------------------+');
180
+ console.log(` | Pair Code: ${code} |`);
181
+ console.log(' +--------------------------+');
182
+ console.log(`\n Or scan QR code with your phone:\n`);
183
+
184
+ qrcode.generate(mobileUrl, { small: true }, (qr) => {
185
+ // Indent each line
186
+ const lines = qr.split('\n').map(l => ' ' + l).join('\n');
187
+ console.log(lines);
188
+ console.log(`\n Mobile URL: ${mobileUrl}`);
189
+ console.log('\n Waiting for phone to connect...\n');
190
+ });
191
+ }
192
+
193
+ function initiateWebRTC() {
194
+ cleanupWebRTC();
195
+
196
+ webrtc = new CompanionWebRTC();
197
+ ptyManager = new PtyManager();
198
+
199
+ // Wire PTY output to WebRTC
200
+ ptyManager.on('send', (msg) => {
201
+ if (webrtc && webrtc.isActive) {
202
+ webrtc.send(msg);
203
+ }
204
+ });
205
+
206
+ // Wire WebRTC messages to PTY
207
+ webrtc.on('message', (msg) => {
208
+ if (msg.type === 'shutdown') {
209
+ console.log('\n Shutdown requested from mobile. Bye!');
210
+ cleanupWebRTC();
211
+ if (ws) ws.close();
212
+ process.exit(0);
213
+ }
214
+ if (msg.type === 'resize') {
215
+ console.log(` [resize] cols=${msg.cols} rows=${msg.rows} sid=${msg.sessionId || 'attached'}`);
216
+ }
217
+ if (ptyManager) {
218
+ const responses = ptyManager.handleMessage(msg);
219
+ for (const resp of responses) {
220
+ webrtc.send(resp);
221
+ }
222
+ }
223
+ });
224
+
225
+ webrtc.on('open', () => {
226
+ console.log(' P2P connection established! Terminal is now accessible from your phone.');
227
+ console.log(' All data flows directly between your devices (encrypted).');
228
+ console.log(' Press Ctrl+C to stop.\n');
229
+ });
230
+
231
+ webrtc.on('close', () => {
232
+ console.log('\n P2P connection closed.');
233
+ cleanupPty();
234
+ scheduleReconnect();
235
+ });
236
+
237
+ webrtc.on('timeout', () => {
238
+ console.log('\n P2P heartbeat timeout. Connection lost.');
239
+ });
240
+
241
+ webrtc.on('error', (err) => {
242
+ console.error(` WebRTC error: ${err}`);
243
+ });
244
+
245
+ // Initiate the connection (sends offer via signaling)
246
+ webrtc.initiate((msg) => {
247
+ if (ws && ws.readyState === WebSocket.OPEN) {
248
+ ws.send(JSON.stringify(msg));
249
+ }
250
+ });
251
+ }
252
+
253
+ function cleanupWebRTC() {
254
+ if (webrtc) {
255
+ webrtc.removeAllListeners();
256
+ webrtc.close();
257
+ webrtc = null;
258
+ }
259
+ cleanupPty();
260
+ }
261
+
262
+ function cleanupPty() {
263
+ if (ptyManager) {
264
+ ptyManager.removeAllListeners();
265
+ ptyManager.destroy();
266
+ ptyManager = null;
267
+ }
268
+ }
269
+
270
+ let reconnectTimer = null;
271
+ let reconnectDelay = 1000; // start at 1s
272
+ const RECONNECT_MAX = 30000; // cap at 30s
273
+
274
+ function scheduleReconnect() {
275
+ if (reconnectTimer) return;
276
+ reconnecting = true;
277
+ const delay = reconnectDelay;
278
+ console.log(` Reconnecting in ${(delay / 1000).toFixed(0)}s...`);
279
+ reconnectTimer = setTimeout(() => {
280
+ reconnectTimer = null;
281
+ connectSignaling();
282
+ }, delay);
283
+ // Exponential backoff: 1s → 2s → 4s → 8s → 16s → 30s (capped)
284
+ reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX);
285
+ }
286
+
287
+ function resetReconnectDelay() {
288
+ reconnectDelay = 1000;
289
+ }
290
+
291
+ // --- Graceful shutdown ---
292
+
293
+ process.on('SIGINT', () => {
294
+ console.log('\n Shutting down...');
295
+ cleanupWebRTC();
296
+ if (ws) {
297
+ ws.close();
298
+ }
299
+ if (reconnectTimer) {
300
+ clearTimeout(reconnectTimer);
301
+ }
302
+ process.exit(0);
303
+ });
304
+
305
+ process.on('SIGTERM', () => {
306
+ cleanupWebRTC();
307
+ if (ws) ws.close();
308
+ process.exit(0);
309
+ });
310
+
311
+ // --- Go ---
312
+ start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghostterm",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Control your PC terminal from your phone — direct P2P, no server in between",
5
5
  "bin": {
6
6
  "ghostterm": "bin/ghostterm-p2p.js"
@@ -14,7 +14,8 @@
14
14
  "dependencies": {
15
15
  "node-datachannel": "^0.12.0",
16
16
  "node-pty": "^1.0.0",
17
- "ws": "^8.0.0"
17
+ "ws": "^8.0.0",
18
+ "qrcode-terminal": "^0.12.0"
18
19
  },
19
20
  "engines": {
20
21
  "node": ">=18.0.0"