ghostterm 1.0.4 → 1.1.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 +22 -6
- package/bin/ghostterm.js +77 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,15 +9,17 @@
|
|
|
9
9
|
## Screenshots
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<img src="https://ghostterm.pages.dev/img/
|
|
13
|
-
<img src="https://ghostterm.pages.dev/img/terminal.jpg" width="250" alt="Terminal with dangerous mode">
|
|
14
|
-
<img src="https://ghostterm.pages.dev/img/pixel-office.jpg" width="250" alt="4 terminal sessions">
|
|
12
|
+
<img src="https://ghostterm.pages.dev/img/banner.png" width="500" alt="GhostTerm Banner">
|
|
15
13
|
</p>
|
|
16
14
|
|
|
17
15
|
<p align="center">
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
<
|
|
16
|
+
<img src="https://ghostterm.pages.dev/img/office-busy.jpg" width="220" alt="Ghosts working">
|
|
17
|
+
<img src="https://ghostterm.pages.dev/img/pixel-office.jpg" width="220" alt="Pixel office overview">
|
|
18
|
+
<img src="https://ghostterm.pages.dev/img/claude-code.jpg" width="220" alt="Claude Code on phone">
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<em>Halloween-themed pixel office · 4 animated ghost terminals · Claude Code on your phone</em>
|
|
21
23
|
</p>
|
|
22
24
|
|
|
23
25
|
---
|
|
@@ -84,6 +86,20 @@ Take a screenshot of your phone screen and send it to your PC terminal. Upload f
|
|
|
84
86
|
| No VPN needed | ✅ | N/A |
|
|
85
87
|
| Google auto-pairing | ✅ | - |
|
|
86
88
|
|
|
89
|
+
## Controls
|
|
90
|
+
|
|
91
|
+
**Top bar**: 4 ghost cells (tap to switch terminals, `+` to create new) · `🏠` pixel office view · `A` font size · `▼` hide controls
|
|
92
|
+
|
|
93
|
+
**Quick keys**: `y` / `n` (approve/deny) · `S+Tab` (shift-tab) · `/ cmd` (slash commands) · `Tab` · `←Bksp`
|
|
94
|
+
|
|
95
|
+
**Left column**: `⏻ claude` (launch menu: new / resume / continue / dangerous mode) · `Stop` (Ctrl+C) · `Close` (kill session)
|
|
96
|
+
|
|
97
|
+
**Center**: D-pad (↑↓←→) + `Enter`
|
|
98
|
+
|
|
99
|
+
**Right column**: `Line↵` (newline) · `⬇` (scroll to bottom) · `Space` · `▲▼` (page up/down) · `Copy` (select mode)
|
|
100
|
+
|
|
101
|
+
**Bottom**: Text input + `Send` · `📷 Shot` (screenshot to PC) · `📁 File` (upload to PC)
|
|
102
|
+
|
|
87
103
|
## Pricing
|
|
88
104
|
|
|
89
105
|
- **Free**: 1 hour/day, full features
|
package/bin/ghostterm.js
CHANGED
|
@@ -37,11 +37,23 @@ function loadToken() {
|
|
|
37
37
|
if (fs.existsSync(CRED_FILE)) {
|
|
38
38
|
const cred = JSON.parse(fs.readFileSync(CRED_FILE, 'utf8'));
|
|
39
39
|
if (cred.id_token) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
// Long token format: base64payload.signature (no dots in payload)
|
|
41
|
+
const parts = cred.id_token.split('.');
|
|
42
|
+
if (parts.length === 2) {
|
|
43
|
+
// Long token — check our own expiry
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(Buffer.from(parts[0], 'base64').toString());
|
|
46
|
+
if (data.exp > Date.now()) return cred.id_token;
|
|
47
|
+
console.log(' Long token expired, need to re-login');
|
|
48
|
+
} catch {}
|
|
49
|
+
} else if (parts.length === 3) {
|
|
50
|
+
// Google JWT — check Google expiry
|
|
51
|
+
try {
|
|
52
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
53
|
+
if (payload.exp * 1000 > Date.now()) return cred.id_token;
|
|
54
|
+
console.log(' Google token expired, need to re-login');
|
|
55
|
+
} catch {}
|
|
43
56
|
}
|
|
44
|
-
console.log(' Token expired, need to re-login');
|
|
45
57
|
}
|
|
46
58
|
}
|
|
47
59
|
} catch {}
|
|
@@ -175,20 +187,28 @@ function createSession() {
|
|
|
175
187
|
|
|
176
188
|
console.log(` Terminal ${id} spawned (PID: ${term.pid})`);
|
|
177
189
|
|
|
190
|
+
function flushOutput() {
|
|
191
|
+
if (session.pendingData) {
|
|
192
|
+
sendToRelay({ type: 'output', data: session.pendingData, seq: session.bufferSeq, id });
|
|
193
|
+
session.pendingData = '';
|
|
194
|
+
}
|
|
195
|
+
session.flushTimer = null;
|
|
196
|
+
}
|
|
197
|
+
|
|
178
198
|
term.onData((data) => {
|
|
179
199
|
session.outputBuffer += data;
|
|
180
200
|
session.bufferSeq += data.length;
|
|
181
201
|
if (session.outputBuffer.length > OUTPUT_BUFFER_MAX) {
|
|
182
202
|
session.outputBuffer = session.outputBuffer.slice(-OUTPUT_BUFFER_MAX);
|
|
183
203
|
}
|
|
184
|
-
// Batch output: accumulate for 12ms then flush once
|
|
185
204
|
session.pendingData += data;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
205
|
+
// Adaptive: small output (keystroke echo) → flush immediately
|
|
206
|
+
// Large output (bulk) → batch for 8ms
|
|
207
|
+
if (session.pendingData.length < 64) {
|
|
208
|
+
if (session.flushTimer) { clearTimeout(session.flushTimer); session.flushTimer = null; }
|
|
209
|
+
flushOutput();
|
|
210
|
+
} else if (!session.flushTimer) {
|
|
211
|
+
session.flushTimer = setTimeout(flushOutput, 8);
|
|
192
212
|
}
|
|
193
213
|
});
|
|
194
214
|
|
|
@@ -208,6 +228,34 @@ function getSessionList() {
|
|
|
208
228
|
return Array.from(sessions.values()).map(s => ({ id: s.id, exited: s.exited }));
|
|
209
229
|
}
|
|
210
230
|
|
|
231
|
+
// ==================== Standby Session (pre-spawn for instant open) ====================
|
|
232
|
+
let standbySession = null;
|
|
233
|
+
|
|
234
|
+
function prepareStandby() {
|
|
235
|
+
// Only prepare if under max and no standby exists
|
|
236
|
+
if (standbySession || sessions.size >= MAX_SESSIONS) return;
|
|
237
|
+
standbySession = createSession();
|
|
238
|
+
// Remove from active sessions — it's hidden until needed
|
|
239
|
+
sessions.delete(standbySession.id);
|
|
240
|
+
console.log(` Standby terminal ${standbySession.id} ready`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function useStandbyOrCreate() {
|
|
244
|
+
let s;
|
|
245
|
+
if (standbySession && !standbySession.exited) {
|
|
246
|
+
s = standbySession;
|
|
247
|
+
sessions.set(s.id, s);
|
|
248
|
+
standbySession = null;
|
|
249
|
+
console.log(` Using standby terminal ${s.id}`);
|
|
250
|
+
} else {
|
|
251
|
+
standbySession = null;
|
|
252
|
+
s = createSession();
|
|
253
|
+
}
|
|
254
|
+
// Prepare next standby after a short delay
|
|
255
|
+
setTimeout(() => prepareStandby(), 1000);
|
|
256
|
+
return s;
|
|
257
|
+
}
|
|
258
|
+
|
|
211
259
|
// ==================== Relay Connection ====================
|
|
212
260
|
let relayWs = null;
|
|
213
261
|
let pairCode = null;
|
|
@@ -267,6 +315,13 @@ function handleRelayMessage(msg) {
|
|
|
267
315
|
case 'auth_ok':
|
|
268
316
|
console.log(` Authenticated as: ${msg.email}`);
|
|
269
317
|
console.log(' Phone will auto-connect with same Google account');
|
|
318
|
+
// Save long token if provided (valid 30 days, no more Google expiry issues)
|
|
319
|
+
if (msg.longToken) {
|
|
320
|
+
saveToken(msg.longToken);
|
|
321
|
+
googleToken = msg.longToken;
|
|
322
|
+
console.log(' Long-lived token saved (30 days)');
|
|
323
|
+
}
|
|
324
|
+
prepareStandby();
|
|
270
325
|
break;
|
|
271
326
|
|
|
272
327
|
case 'auth_error':
|
|
@@ -283,10 +338,16 @@ function handleRelayMessage(msg) {
|
|
|
283
338
|
|
|
284
339
|
case 'mobile_connected':
|
|
285
340
|
console.log(' Mobile connected!');
|
|
286
|
-
if (sessions.size === 0)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
341
|
+
if (sessions.size === 0) {
|
|
342
|
+
// Use standby session if available, otherwise create new
|
|
343
|
+
const s = useStandbyOrCreate();
|
|
344
|
+
sendToRelay({ type: 'sessions', list: getSessionList() });
|
|
345
|
+
sendToRelay({ type: 'attached', id: s.id });
|
|
346
|
+
} else {
|
|
347
|
+
sendToRelay({ type: 'sessions', list: getSessionList() });
|
|
348
|
+
const first = sessions.values().next().value;
|
|
349
|
+
if (first) sendToRelay({ type: 'attached', id: first.id });
|
|
350
|
+
}
|
|
290
351
|
break;
|
|
291
352
|
|
|
292
353
|
case 'mobile_disconnected':
|
|
@@ -311,7 +372,7 @@ function handleRelayMessage(msg) {
|
|
|
311
372
|
|
|
312
373
|
case 'create-session':
|
|
313
374
|
if (sessions.size < MAX_SESSIONS) {
|
|
314
|
-
const session =
|
|
375
|
+
const session = useStandbyOrCreate();
|
|
315
376
|
sendToRelay({ type: 'sessions', list: getSessionList() });
|
|
316
377
|
sendToRelay({ type: 'attached', id: session.id });
|
|
317
378
|
} else {
|