forkoff 1.0.19 → 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 +7 -5
- package/dist/cloud-client.d.ts +30 -0
- package/dist/cloud-client.js +165 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +24 -0
- package/dist/crypto/e2eeManager.d.ts +8 -0
- package/dist/crypto/e2eeManager.js +90 -14
- package/dist/crypto/index.d.ts +1 -1
- package/dist/crypto/index.js +2 -1
- package/dist/crypto/keyExchange.d.ts +18 -0
- package/dist/crypto/keyExchange.js +37 -1
- package/dist/crypto/keyStorage.d.ts +4 -3
- package/dist/crypto/keyStorage.js +6 -4
- package/dist/crypto/sessionPersistence.js +24 -2
- package/dist/crypto/types.d.ts +3 -1
- package/dist/index.js +142 -30
- package/dist/websocket.d.ts +14 -1
- package/dist/websocket.js +159 -42
- package/package.json +3 -1
package/dist/websocket.js
CHANGED
|
@@ -6,6 +6,7 @@ const events_1 = require("events");
|
|
|
6
6
|
const uuid_1 = require("uuid");
|
|
7
7
|
const e2eeManager_1 = require("./crypto/e2eeManager");
|
|
8
8
|
const server_1 = require("./server");
|
|
9
|
+
const cloud_client_1 = require("./cloud-client");
|
|
9
10
|
// Whitelist of events allowed via encrypted_message channel (inbound from mobile)
|
|
10
11
|
const ALLOWED_ENCRYPTED_EVENTS = new Set([
|
|
11
12
|
'terminal_command',
|
|
@@ -35,36 +36,40 @@ const ALLOWED_ENCRYPTED_EVENTS = new Set([
|
|
|
35
36
|
// Events that carry user data and MUST be encrypted — plaintext fallback is refused.
|
|
36
37
|
// If E2EE is not established, these are queued (not sent in plaintext).
|
|
37
38
|
const ENFORCED_SENSITIVE_EVENTS = new Set([
|
|
38
|
-
// Core sensitive data
|
|
39
|
+
// Core sensitive data — these truly contain code/file contents
|
|
39
40
|
'terminal_output',
|
|
40
41
|
'read_file_response',
|
|
41
|
-
'directory_list_response',
|
|
42
42
|
'permission_prompt',
|
|
43
43
|
// Transcript data (contains full code, file contents, conversation history)
|
|
44
44
|
'transcript_history',
|
|
45
45
|
'transcript_update',
|
|
46
|
-
// Claude reasoning
|
|
46
|
+
// Claude reasoning
|
|
47
47
|
'thinking_content',
|
|
48
|
-
|
|
49
|
-
'tool_activity',
|
|
50
|
-
// Approval context
|
|
48
|
+
// Approval context (contains code details)
|
|
51
49
|
'claude_approval_request',
|
|
52
50
|
'approval_request',
|
|
53
|
-
//
|
|
51
|
+
// Pending permissions (contains prompt details)
|
|
52
|
+
'pending_permissions_sync',
|
|
53
|
+
// Session metadata (contains directory paths, session identifiers)
|
|
54
54
|
'claude_session_update',
|
|
55
55
|
'claude_session_batch_update',
|
|
56
|
+
'claude_session_event',
|
|
57
|
+
// Directory/file data
|
|
58
|
+
'directory_list_response',
|
|
56
59
|
'terminal_cwd',
|
|
57
60
|
'file_changed',
|
|
58
|
-
// Token
|
|
61
|
+
// Token/usage/progress data
|
|
59
62
|
'token_usage',
|
|
60
|
-
|
|
61
|
-
'
|
|
62
|
-
//
|
|
63
|
-
'claude_session_event',
|
|
64
|
-
// Usage analytics sync
|
|
63
|
+
'task_progress',
|
|
64
|
+
'tool_activity',
|
|
65
|
+
// Usage analytics
|
|
65
66
|
'usage_stats_sync',
|
|
66
67
|
'daily_usage_sync',
|
|
67
68
|
'streak_info_sync',
|
|
69
|
+
// Tool status (contains tool state)
|
|
70
|
+
'tool_status_update',
|
|
71
|
+
// Session loading state signal
|
|
72
|
+
'session_loading',
|
|
68
73
|
]);
|
|
69
74
|
// SECURITY: Inbound events from mobile that MUST arrive via E2EE decryption when active.
|
|
70
75
|
// Plaintext versions are dropped when E2EE session is established with mobile peer.
|
|
@@ -84,7 +89,7 @@ const PLAINTEXT_DROP_EVENTS = [
|
|
|
84
89
|
'directory_list', 'transcript_fetch', 'transcript_subscribe',
|
|
85
90
|
'read_file', 'claude_approval_response', 'permission_response',
|
|
86
91
|
'permission_rules_sync', 'claude_abort', 'tab_complete',
|
|
87
|
-
'usage_stats_request',
|
|
92
|
+
'usage_stats_request', 'sdk_session_history',
|
|
88
93
|
];
|
|
89
94
|
/** Events forwarded from server that do NOT need plaintext-drop checking */
|
|
90
95
|
const PASSTHROUGH_EVENTS = [
|
|
@@ -96,10 +101,14 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
96
101
|
this.server = null;
|
|
97
102
|
this.heartbeatInterval = null;
|
|
98
103
|
this._sessionId = '';
|
|
104
|
+
this.isCloudRelay = false;
|
|
99
105
|
this.e2eeManager = null;
|
|
100
106
|
this.e2eeInitialized = false;
|
|
101
107
|
// The mobile device ID learned from key exchange (used for encrypting CLI→mobile messages)
|
|
102
108
|
this.e2eePeerDeviceId = null;
|
|
109
|
+
this._keyExchangePending = false;
|
|
110
|
+
this._keyExchangeDebounceTimer = null;
|
|
111
|
+
this._keyExchangeDebounceTarget = null;
|
|
103
112
|
// Queue for sensitive messages waiting for E2EE session establishment
|
|
104
113
|
this.pendingSensitiveMessages = [];
|
|
105
114
|
this.usageTracker = null;
|
|
@@ -117,38 +126,89 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
117
126
|
if (!deviceId) {
|
|
118
127
|
throw new Error('Device not registered');
|
|
119
128
|
}
|
|
129
|
+
this.isCloudRelay = false;
|
|
120
130
|
this.server = new server_1.EmbeddedRelayServer({
|
|
121
131
|
port,
|
|
122
132
|
deviceId,
|
|
123
133
|
deviceName: config_1.config.deviceName,
|
|
124
134
|
});
|
|
125
135
|
await this.server.start();
|
|
136
|
+
this.wireUpTransportEvents();
|
|
137
|
+
}
|
|
138
|
+
/** Connect to a cloud relay as a Socket.io client (instead of running a local server) */
|
|
139
|
+
async connectToRelay(url) {
|
|
140
|
+
const deviceId = config_1.config.deviceId;
|
|
141
|
+
if (!deviceId) {
|
|
142
|
+
throw new Error('Device not registered');
|
|
143
|
+
}
|
|
144
|
+
this.isCloudRelay = true;
|
|
145
|
+
const cloudClient = new cloud_client_1.CloudRelayClient({
|
|
146
|
+
url,
|
|
147
|
+
deviceId,
|
|
148
|
+
deviceName: config_1.config.deviceName,
|
|
149
|
+
relayToken: config_1.config.relayToken,
|
|
150
|
+
});
|
|
151
|
+
this.server = cloudClient;
|
|
152
|
+
await cloudClient.start();
|
|
153
|
+
this.wireUpTransportEvents();
|
|
154
|
+
}
|
|
155
|
+
/** Wire up event forwarding from the transport (shared between startServer and connectToRelay) */
|
|
156
|
+
wireUpTransportEvents() {
|
|
157
|
+
if (!this.server)
|
|
158
|
+
return;
|
|
126
159
|
// When mobile connects, emit connected + start heartbeat + initiate E2EE
|
|
127
160
|
this.server.on('mobile_connected', (data) => {
|
|
128
161
|
console.log(`[WS] Mobile connected: ${data.deviceId}`);
|
|
129
162
|
this.emit('connected');
|
|
130
163
|
this.startHeartbeat();
|
|
131
|
-
//
|
|
132
|
-
|
|
164
|
+
// SECURITY: Clear stale E2EE peer state on every mobile connect.
|
|
165
|
+
// Mobile sessions are in-memory only — lost on app restart/reconnect.
|
|
166
|
+
// Without this, CLI would encrypt with a restored persisted session
|
|
167
|
+
// that mobile can't decrypt, causing "No session established" errors.
|
|
168
|
+
// A fresh key exchange will re-establish the session after debounce.
|
|
169
|
+
if (this.e2eePeerDeviceId) {
|
|
170
|
+
console.log(`[E2EE] Clearing stale peer state for ${this.e2eePeerDeviceId} (mobile reconnected)`);
|
|
171
|
+
this.e2eeManager?.clearSession(this.e2eePeerDeviceId);
|
|
172
|
+
this.e2eePeerDeviceId = null;
|
|
173
|
+
}
|
|
174
|
+
// Debounce E2EE key exchange initiation.
|
|
175
|
+
// Relay fires multiple mobile_connected events with different IDs per connection cycle.
|
|
176
|
+
// Wait for them to settle, then initiate ONE key exchange with the last ID.
|
|
177
|
+
// NOTE: Don't guard on e2eeInitialized here — initE2EE() is async and may not
|
|
178
|
+
// be done when mobile_connected fires. Check inside the callback instead;
|
|
179
|
+
// by the time the 1.5s debounce fires, initE2EE() will have completed.
|
|
180
|
+
if (this._keyExchangeDebounceTimer) {
|
|
181
|
+
clearTimeout(this._keyExchangeDebounceTimer);
|
|
182
|
+
}
|
|
183
|
+
this._keyExchangeDebounceTarget = data.deviceId;
|
|
184
|
+
this._keyExchangeDebounceTimer = setTimeout(() => {
|
|
185
|
+
this._keyExchangeDebounceTimer = null;
|
|
186
|
+
const targetId = this._keyExchangeDebounceTarget;
|
|
187
|
+
if (!targetId || !this.e2eeManager || !this.e2eeInitialized)
|
|
188
|
+
return;
|
|
133
189
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
this.e2eeManager.
|
|
138
|
-
const initPayload = this.e2eeManager.createKeyExchangeInit(data.deviceId);
|
|
190
|
+
this._keyExchangePending = true;
|
|
191
|
+
console.log(`[E2EE] Initiating key exchange with ${targetId} (after debounce)`);
|
|
192
|
+
this.e2eeManager.clearSession(targetId);
|
|
193
|
+
const initPayload = this.e2eeManager.createKeyExchangeInit(targetId);
|
|
139
194
|
this.server?.emitToMobile('encrypted_key_exchange_init', {
|
|
140
195
|
...initPayload,
|
|
141
|
-
recipientDeviceId:
|
|
196
|
+
recipientDeviceId: targetId,
|
|
142
197
|
});
|
|
143
|
-
// E2EE key exchange initiated
|
|
144
198
|
}
|
|
145
199
|
catch (err) {
|
|
200
|
+
this._keyExchangePending = false;
|
|
146
201
|
console.warn('[E2EE] Failed to initiate key exchange');
|
|
147
202
|
}
|
|
148
|
-
}
|
|
203
|
+
}, 1500); // 1.5s debounce — wait for all mobile_connected events to settle
|
|
149
204
|
});
|
|
150
205
|
this.server.on('mobile_disconnected', (data) => {
|
|
151
206
|
console.log(`[WS] Mobile disconnected: ${data.reason}`);
|
|
207
|
+
if (this._keyExchangeDebounceTimer) {
|
|
208
|
+
clearTimeout(this._keyExchangeDebounceTimer);
|
|
209
|
+
this._keyExchangeDebounceTimer = null;
|
|
210
|
+
}
|
|
211
|
+
this._keyExchangePending = false;
|
|
152
212
|
this.emit('disconnected', data.reason);
|
|
153
213
|
this.stopHeartbeat();
|
|
154
214
|
});
|
|
@@ -172,20 +232,24 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
172
232
|
this.emit(event, data);
|
|
173
233
|
});
|
|
174
234
|
}
|
|
175
|
-
// On successful pairing, reset TOFU trust
|
|
176
|
-
// Don't delete pending exchange or re-initiate — the init from mobile_connected is in-flight.
|
|
235
|
+
// On successful pairing, reset TOFU trust
|
|
177
236
|
this.server.on('pair_device', (data) => {
|
|
178
237
|
const mobileDeviceId = data.mobileDeviceId;
|
|
179
238
|
if (mobileDeviceId && this.e2eeManager) {
|
|
180
239
|
this.e2eeManager.clearTrustOnly(mobileDeviceId);
|
|
181
|
-
// Reset TOFU trust for re-pair
|
|
182
240
|
}
|
|
183
241
|
});
|
|
184
|
-
// E2EE key exchange events
|
|
242
|
+
// E2EE key exchange events
|
|
185
243
|
this.server.on('encrypted_key_exchange_init', (data) => {
|
|
186
244
|
if (!this.e2eeManager)
|
|
187
245
|
return;
|
|
246
|
+
// Guard: ignore our own init echoed back by the relay
|
|
247
|
+
if (data.senderDeviceId === config_1.config.deviceId) {
|
|
248
|
+
console.log('[E2EE] Ignoring own key exchange init echo');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
188
251
|
try {
|
|
252
|
+
console.log(`[E2EE] Received key exchange init from ${data.senderDeviceId}`);
|
|
189
253
|
const ack = this.e2eeManager.handleKeyExchangeInit(data);
|
|
190
254
|
this.e2eePeerDeviceId = data.senderDeviceId;
|
|
191
255
|
this.server?.emitToMobile('encrypted_key_exchange_ack', {
|
|
@@ -198,21 +262,33 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
198
262
|
this.sendAllUsageStats();
|
|
199
263
|
}
|
|
200
264
|
catch (err) {
|
|
201
|
-
|
|
265
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
266
|
+
console.error(`[E2EE] Key exchange init failed: ${msg}`);
|
|
202
267
|
}
|
|
203
268
|
});
|
|
204
269
|
this.server.on('encrypted_key_exchange_ack', (data) => {
|
|
205
270
|
if (!this.e2eeManager)
|
|
206
271
|
return;
|
|
207
272
|
try {
|
|
273
|
+
console.log(`[E2EE] Received key exchange ack from ${data.senderDeviceId}`);
|
|
208
274
|
this.e2eeManager.handleKeyExchangeAck(data);
|
|
275
|
+
this._keyExchangePending = false;
|
|
209
276
|
this.e2eePeerDeviceId = data.senderDeviceId;
|
|
277
|
+
console.log(`[E2EE] Key exchange complete — session established with ${data.senderDeviceId}`);
|
|
210
278
|
this.emit('e2ee_established', { peerDeviceId: data.senderDeviceId });
|
|
211
279
|
this.flushSensitiveQueue();
|
|
212
280
|
this.sendAllUsageStats();
|
|
213
281
|
}
|
|
214
282
|
catch (err) {
|
|
215
|
-
|
|
283
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
284
|
+
// Only suppress if it's truly a duplicate ack (pending exchange already consumed)
|
|
285
|
+
if (msg.includes('No pending key exchange')) {
|
|
286
|
+
console.log(`[E2EE] Duplicate ack from ${data.senderDeviceId} — ignored`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
console.error(`[E2EE] Key exchange ack failed: ${msg}`);
|
|
290
|
+
this._keyExchangePending = false;
|
|
291
|
+
}
|
|
216
292
|
}
|
|
217
293
|
});
|
|
218
294
|
// Encrypted messages — decrypt and re-emit as original event
|
|
@@ -227,7 +303,6 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
227
303
|
console.error('[E2EE] Decryption failed — message dropped');
|
|
228
304
|
return;
|
|
229
305
|
}
|
|
230
|
-
// Validate JSON structure separately from decryption
|
|
231
306
|
let parsed;
|
|
232
307
|
try {
|
|
233
308
|
parsed = JSON.parse(plaintext);
|
|
@@ -236,7 +311,6 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
236
311
|
console.error('[E2EE] Invalid JSON in decrypted message — dropped');
|
|
237
312
|
return;
|
|
238
313
|
}
|
|
239
|
-
// Validate payload structure
|
|
240
314
|
if (!parsed || typeof parsed !== 'object') {
|
|
241
315
|
console.error('[E2EE] Decrypted payload is not an object — dropped');
|
|
242
316
|
return;
|
|
@@ -253,7 +327,7 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
253
327
|
}
|
|
254
328
|
this.emit(eventName, payload._data);
|
|
255
329
|
});
|
|
256
|
-
// Initialize E2EE (non-blocking
|
|
330
|
+
// Initialize E2EE (non-blocking)
|
|
257
331
|
this.initE2EE().catch((err) => {
|
|
258
332
|
console.warn('[E2EE] \u26a0 End-to-end encryption initialization failed. Messages will be sent without E2EE protection.');
|
|
259
333
|
if (process.env.DEBUG) {
|
|
@@ -261,7 +335,7 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
261
335
|
}
|
|
262
336
|
});
|
|
263
337
|
}
|
|
264
|
-
/** Set pairing code on the
|
|
338
|
+
/** Set pairing code on the transport for validation */
|
|
265
339
|
setPairingCode(code) {
|
|
266
340
|
this.server?.setPairingCode(code);
|
|
267
341
|
}
|
|
@@ -301,6 +375,35 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
301
375
|
return;
|
|
302
376
|
}
|
|
303
377
|
catch (err) {
|
|
378
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
379
|
+
if (errMsg.includes('re-key required')) {
|
|
380
|
+
// Session expired — automatically initiate a new key exchange
|
|
381
|
+
console.warn(`[E2EE] Session expired with ${targetDeviceId} — initiating re-key`);
|
|
382
|
+
this.e2eePeerDeviceId = null;
|
|
383
|
+
try {
|
|
384
|
+
this._keyExchangePending = true;
|
|
385
|
+
this.e2eeManager.clearSession(targetDeviceId);
|
|
386
|
+
const initPayload = this.e2eeManager.createKeyExchangeInit(targetDeviceId);
|
|
387
|
+
this.server?.emitToMobile('encrypted_key_exchange_init', {
|
|
388
|
+
...initPayload,
|
|
389
|
+
recipientDeviceId: targetDeviceId,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
catch (reKeyErr) {
|
|
393
|
+
this._keyExchangePending = false;
|
|
394
|
+
console.error('[E2EE] Re-key initiation failed');
|
|
395
|
+
}
|
|
396
|
+
// Queue the original message so it can be sent once re-keying completes
|
|
397
|
+
if (this.pendingSensitiveMessages.length < WebSocketClient.MAX_PENDING_SENSITIVE) {
|
|
398
|
+
this.pendingSensitiveMessages.push({
|
|
399
|
+
event,
|
|
400
|
+
data,
|
|
401
|
+
targetDeviceId,
|
|
402
|
+
queuedAt: Date.now(),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
304
407
|
console.error('[E2EE] Encryption failed, message NOT sent (refusing plaintext fallback)');
|
|
305
408
|
return;
|
|
306
409
|
}
|
|
@@ -357,7 +460,9 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
357
460
|
sent++;
|
|
358
461
|
}
|
|
359
462
|
this.pendingSensitiveMessages = [];
|
|
360
|
-
|
|
463
|
+
if (sent > 0 || dropped > 0) {
|
|
464
|
+
console.log(`[E2EE] Flushed sensitive queue: ${sent} sent, ${dropped} expired`);
|
|
465
|
+
}
|
|
361
466
|
}
|
|
362
467
|
/** Get the E2EE manager (for external key exchange initiation) */
|
|
363
468
|
getE2EEManager() {
|
|
@@ -369,6 +474,10 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
369
474
|
}
|
|
370
475
|
/** SECURITY: Check if plaintext inbound events should be dropped (E2EE active with peer) */
|
|
371
476
|
shouldDropPlaintextInbound() {
|
|
477
|
+
// In cloud relay mode, events arrive via API forwarding — trusted, don't drop
|
|
478
|
+
if (this.isCloudRelay)
|
|
479
|
+
return false;
|
|
480
|
+
// In embedded relay mode, mobile connects directly — drop plaintext when E2EE active
|
|
372
481
|
return !!(this.e2eePeerDeviceId && this.e2eeManager?.hasSessionKey(this.e2eePeerDeviceId));
|
|
373
482
|
}
|
|
374
483
|
disconnect() {
|
|
@@ -377,6 +486,7 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
377
486
|
this.e2eeManager = null;
|
|
378
487
|
this.e2eeInitialized = false;
|
|
379
488
|
this.e2eePeerDeviceId = null;
|
|
489
|
+
this.isCloudRelay = false;
|
|
380
490
|
if (this.server) {
|
|
381
491
|
this.server.stop();
|
|
382
492
|
this.server = null;
|
|
@@ -393,11 +503,11 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
393
503
|
this.heartbeatInterval = null;
|
|
394
504
|
}
|
|
395
505
|
}
|
|
396
|
-
// Send heartbeat to connected mobile
|
|
506
|
+
// Send heartbeat to connected mobile (plaintext — only carries status, no sensitive data)
|
|
397
507
|
sendHeartbeat(status = 'online') {
|
|
398
508
|
this.server?.emitToMobile('device_status', { status, deviceId: config_1.config.deviceId });
|
|
399
509
|
}
|
|
400
|
-
// Update device status
|
|
510
|
+
// Update device status (plaintext — only carries status, no sensitive data)
|
|
401
511
|
updateStatus(status) {
|
|
402
512
|
this.server?.emitToMobile('device_status', { status, deviceId: config_1.config.deviceId });
|
|
403
513
|
}
|
|
@@ -408,17 +518,20 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
408
518
|
// Send multiple Claude sessions as a single batch event (sensitive — contains directory paths)
|
|
409
519
|
sendClaudeSessions(sessions) {
|
|
410
520
|
if (sessions.length > 0) {
|
|
521
|
+
const peer = this.e2eePeerDeviceId;
|
|
522
|
+
const hasE2EE = peer ? this.e2eeManager?.hasSessionKey(peer) : false;
|
|
523
|
+
console.log(`[WS] sendClaudeSessions: ${sessions.length} sessions, peer=${peer || 'none'}, e2ee=${hasE2EE}, queue=${this.pendingSensitiveMessages.length}`);
|
|
411
524
|
const withDeviceId = sessions.map(s => ({ ...s, deviceId: config_1.config.deviceId }));
|
|
412
|
-
this.emitSensitive('claude_session_batch_update', { sessions: withDeviceId },
|
|
525
|
+
this.emitSensitive('claude_session_batch_update', { sessions: withDeviceId }, peer ?? undefined);
|
|
413
526
|
}
|
|
414
527
|
}
|
|
415
|
-
// Send tool status update (e.g., Claude active/inactive)
|
|
528
|
+
// Send tool status update (e.g., Claude active/inactive) — encrypted via E2EE
|
|
416
529
|
sendToolStatusUpdate(toolType, status) {
|
|
417
|
-
this.
|
|
530
|
+
this.emitSensitive('tool_status_update', {
|
|
418
531
|
toolType,
|
|
419
532
|
status,
|
|
420
533
|
timestamp: new Date().toISOString(),
|
|
421
|
-
});
|
|
534
|
+
}, this.e2eePeerDeviceId ?? undefined);
|
|
422
535
|
}
|
|
423
536
|
// Send approval request (sensitive — contains code change descriptions)
|
|
424
537
|
sendApprovalRequest(data) {
|
|
@@ -511,6 +624,10 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
511
624
|
this.emitSensitive('streak_info_sync', { ...streak, deviceId }, this.e2eePeerDeviceId ?? undefined);
|
|
512
625
|
console.log(`[WS] Sent usage stats sync to mobile`);
|
|
513
626
|
}
|
|
627
|
+
// Send session loading state to mobile (so mobile shows/hides loading indicator)
|
|
628
|
+
sendSessionLoading(data) {
|
|
629
|
+
this.emitSensitive('session_loading', data, this.e2eePeerDeviceId ?? undefined);
|
|
630
|
+
}
|
|
514
631
|
// Send Claude session event (sensitive — may contain error messages with paths)
|
|
515
632
|
sendClaudeSessionEvent(data) {
|
|
516
633
|
this.emitSensitive('claude_session_event', data, this.e2eePeerDeviceId ?? undefined);
|
|
@@ -520,7 +637,7 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
520
637
|
}
|
|
521
638
|
}
|
|
522
639
|
exports.WebSocketClient = WebSocketClient;
|
|
523
|
-
WebSocketClient.SENSITIVE_QUEUE_TTL_MS =
|
|
640
|
+
WebSocketClient.SENSITIVE_QUEUE_TTL_MS = 120000; // 2 minutes max wait
|
|
524
641
|
WebSocketClient.MAX_PENDING_SENSITIVE = 200;
|
|
525
642
|
exports.wsClient = new WebSocketClient();
|
|
526
643
|
exports.default = exports.wsClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forkoff",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI tool to connect your AI coding tools to mobile | Open Beta - Download the app: https://testflight.apple.com/join/dhh5FrN7",
|
|
5
5
|
"main": "dist/integration.js",
|
|
6
6
|
"types": "dist/integration.d.ts",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"license": "MIT",
|
|
52
52
|
"dependencies": {
|
|
53
|
+
"@noble/hashes": "^1.8.0",
|
|
53
54
|
"chalk": "^4.1.2",
|
|
54
55
|
"chokidar": "^3.6.0",
|
|
55
56
|
"commander": "^14.0.2",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"ora": "^5.4.1",
|
|
61
62
|
"qrcode-terminal": "^0.12.0",
|
|
62
63
|
"socket.io": "^4.8.3",
|
|
64
|
+
"socket.io-client": "^4.8.3",
|
|
63
65
|
"tweetnacl": "^1.0.3",
|
|
64
66
|
"tweetnacl-util": "^0.15.1",
|
|
65
67
|
"uuid": "^8.3.2"
|