ghostterm 1.0.4 → 1.1.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/README.md +14 -0
- package/bin/ghostterm.js +56 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,20 @@ Take a screenshot of your phone screen and send it to your PC terminal. Upload f
|
|
|
84
84
|
| No VPN needed | ✅ | N/A |
|
|
85
85
|
| Google auto-pairing | ✅ | - |
|
|
86
86
|
|
|
87
|
+
## Controls
|
|
88
|
+
|
|
89
|
+
**Top bar**: 4 ghost cells (tap to switch terminals, `+` to create new) · `🏠` pixel office view · `A` font size · `▼` hide controls
|
|
90
|
+
|
|
91
|
+
**Quick keys**: `y` / `n` (approve/deny) · `S+Tab` (shift-tab) · `/ cmd` (slash commands) · `Tab` · `←Bksp`
|
|
92
|
+
|
|
93
|
+
**Left column**: `⏻ claude` (launch menu: new / resume / continue / dangerous mode) · `Stop` (Ctrl+C) · `Close` (kill session)
|
|
94
|
+
|
|
95
|
+
**Center**: D-pad (↑↓←→) + `Enter`
|
|
96
|
+
|
|
97
|
+
**Right column**: `Line↵` (newline) · `⬇` (scroll to bottom) · `Space` · `▲▼` (page up/down) · `Copy` (select mode)
|
|
98
|
+
|
|
99
|
+
**Bottom**: Text input + `Send` · `📷 Shot` (screenshot to PC) · `📁 File` (upload to PC)
|
|
100
|
+
|
|
87
101
|
## Pricing
|
|
88
102
|
|
|
89
103
|
- **Free**: 1 hour/day, full features
|
package/bin/ghostterm.js
CHANGED
|
@@ -175,20 +175,28 @@ function createSession() {
|
|
|
175
175
|
|
|
176
176
|
console.log(` Terminal ${id} spawned (PID: ${term.pid})`);
|
|
177
177
|
|
|
178
|
+
function flushOutput() {
|
|
179
|
+
if (session.pendingData) {
|
|
180
|
+
sendToRelay({ type: 'output', data: session.pendingData, seq: session.bufferSeq, id });
|
|
181
|
+
session.pendingData = '';
|
|
182
|
+
}
|
|
183
|
+
session.flushTimer = null;
|
|
184
|
+
}
|
|
185
|
+
|
|
178
186
|
term.onData((data) => {
|
|
179
187
|
session.outputBuffer += data;
|
|
180
188
|
session.bufferSeq += data.length;
|
|
181
189
|
if (session.outputBuffer.length > OUTPUT_BUFFER_MAX) {
|
|
182
190
|
session.outputBuffer = session.outputBuffer.slice(-OUTPUT_BUFFER_MAX);
|
|
183
191
|
}
|
|
184
|
-
// Batch output: accumulate for 12ms then flush once
|
|
185
192
|
session.pendingData += data;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
// Adaptive: small output (keystroke echo) → flush immediately
|
|
194
|
+
// Large output (bulk) → batch for 8ms
|
|
195
|
+
if (session.pendingData.length < 64) {
|
|
196
|
+
if (session.flushTimer) { clearTimeout(session.flushTimer); session.flushTimer = null; }
|
|
197
|
+
flushOutput();
|
|
198
|
+
} else if (!session.flushTimer) {
|
|
199
|
+
session.flushTimer = setTimeout(flushOutput, 8);
|
|
192
200
|
}
|
|
193
201
|
});
|
|
194
202
|
|
|
@@ -208,6 +216,34 @@ function getSessionList() {
|
|
|
208
216
|
return Array.from(sessions.values()).map(s => ({ id: s.id, exited: s.exited }));
|
|
209
217
|
}
|
|
210
218
|
|
|
219
|
+
// ==================== Standby Session (pre-spawn for instant open) ====================
|
|
220
|
+
let standbySession = null;
|
|
221
|
+
|
|
222
|
+
function prepareStandby() {
|
|
223
|
+
// Only prepare if under max and no standby exists
|
|
224
|
+
if (standbySession || sessions.size >= MAX_SESSIONS) return;
|
|
225
|
+
standbySession = createSession();
|
|
226
|
+
// Remove from active sessions — it's hidden until needed
|
|
227
|
+
sessions.delete(standbySession.id);
|
|
228
|
+
console.log(` Standby terminal ${standbySession.id} ready`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function useStandbyOrCreate() {
|
|
232
|
+
let s;
|
|
233
|
+
if (standbySession && !standbySession.exited) {
|
|
234
|
+
s = standbySession;
|
|
235
|
+
sessions.set(s.id, s);
|
|
236
|
+
standbySession = null;
|
|
237
|
+
console.log(` Using standby terminal ${s.id}`);
|
|
238
|
+
} else {
|
|
239
|
+
standbySession = null;
|
|
240
|
+
s = createSession();
|
|
241
|
+
}
|
|
242
|
+
// Prepare next standby after a short delay
|
|
243
|
+
setTimeout(() => prepareStandby(), 1000);
|
|
244
|
+
return s;
|
|
245
|
+
}
|
|
246
|
+
|
|
211
247
|
// ==================== Relay Connection ====================
|
|
212
248
|
let relayWs = null;
|
|
213
249
|
let pairCode = null;
|
|
@@ -267,6 +303,8 @@ function handleRelayMessage(msg) {
|
|
|
267
303
|
case 'auth_ok':
|
|
268
304
|
console.log(` Authenticated as: ${msg.email}`);
|
|
269
305
|
console.log(' Phone will auto-connect with same Google account');
|
|
306
|
+
// Pre-spawn a standby terminal so first open is instant
|
|
307
|
+
prepareStandby();
|
|
270
308
|
break;
|
|
271
309
|
|
|
272
310
|
case 'auth_error':
|
|
@@ -283,10 +321,16 @@ function handleRelayMessage(msg) {
|
|
|
283
321
|
|
|
284
322
|
case 'mobile_connected':
|
|
285
323
|
console.log(' Mobile connected!');
|
|
286
|
-
if (sessions.size === 0)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
324
|
+
if (sessions.size === 0) {
|
|
325
|
+
// Use standby session if available, otherwise create new
|
|
326
|
+
const s = useStandbyOrCreate();
|
|
327
|
+
sendToRelay({ type: 'sessions', list: getSessionList() });
|
|
328
|
+
sendToRelay({ type: 'attached', id: s.id });
|
|
329
|
+
} else {
|
|
330
|
+
sendToRelay({ type: 'sessions', list: getSessionList() });
|
|
331
|
+
const first = sessions.values().next().value;
|
|
332
|
+
if (first) sendToRelay({ type: 'attached', id: first.id });
|
|
333
|
+
}
|
|
290
334
|
break;
|
|
291
335
|
|
|
292
336
|
case 'mobile_disconnected':
|
|
@@ -311,7 +355,7 @@ function handleRelayMessage(msg) {
|
|
|
311
355
|
|
|
312
356
|
case 'create-session':
|
|
313
357
|
if (sessions.size < MAX_SESSIONS) {
|
|
314
|
-
const session =
|
|
358
|
+
const session = useStandbyOrCreate();
|
|
315
359
|
sendToRelay({ type: 'sessions', list: getSessionList() });
|
|
316
360
|
sendToRelay({ type: 'attached', id: session.id });
|
|
317
361
|
} else {
|