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 CHANGED
@@ -9,15 +9,17 @@
9
9
  ## Screenshots
10
10
 
11
11
  <p align="center">
12
- <img src="https://ghostterm.pages.dev/img/claude-code.jpg" width="250" alt="Claude Code running on phone">
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
- <em>Left: Claude Code with <code>--dangerously-skip-permissions</code> running on your phone</em><br>
19
- <em>Center: Full terminal + custom shortcut keys (y/n, Tab, Ctrl+C, arrows...)</em><br>
20
- <em>Right: 4 ghost terminals in pixel office view</em>
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
- const payload = JSON.parse(Buffer.from(cred.id_token.split('.')[1], 'base64').toString());
41
- if (payload.exp * 1000 > Date.now()) {
42
- return cred.id_token;
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
- if (!session.flushTimer) {
187
- session.flushTimer = setTimeout(() => {
188
- sendToRelay({ type: 'output', data: session.pendingData, seq: session.bufferSeq, id });
189
- session.pendingData = '';
190
- session.flushTimer = null;
191
- }, 12);
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) createSession();
287
- sendToRelay({ type: 'sessions', list: getSessionList() });
288
- const first = sessions.values().next().value;
289
- if (first) sendToRelay({ type: 'attached', id: first.id });
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 = createSession();
375
+ const session = useStandbyOrCreate();
315
376
  sendToRelay({ type: 'sessions', list: getSessionList() });
316
377
  sendToRelay({ type: 'attached', id: session.id });
317
378
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghostterm",
3
- "version": "1.0.4",
3
+ "version": "1.1.1",
4
4
  "description": "Mobile terminal for Claude Code — control your PC from your phone",
5
5
  "bin": {
6
6
  "ghostterm": "bin/ghostterm.js"