forkoff 1.1.4 → 1.1.5

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/dist/index.js CHANGED
@@ -1098,7 +1098,7 @@ function createProgram() {
1098
1098
  websocket_1.wsClient.on('disconnected', (reason) => {
1099
1099
  console.log(chalk_1.default.yellow(`\nMobile disconnected: ${reason}`));
1100
1100
  console.log(chalk_1.default.dim('Waiting for mobile to reconnect...'));
1101
- tools_1.claudeProcessManager.autoAllowAllPendingPrompts();
1101
+ tools_1.claudeProcessManager.resolveAllPendingPrompts('deny', 'mobile disconnected');
1102
1102
  tools_1.claudeProcessManager.cleanupAllPermissionState();
1103
1103
  tools_1.claudeProcessManager.clearAllTakenOver();
1104
1104
  });
@@ -1136,15 +1136,17 @@ function createProgram() {
1136
1136
  // Helper function to wait for pairing via WebSocket event
1137
1137
  async function waitForPairing() {
1138
1138
  return new Promise((resolve) => {
1139
- websocket_1.wsClient.on('pair_device', (data) => {
1140
- resolve({ mobileDeviceId: data.mobileDeviceId });
1141
- });
1142
- // Handle Ctrl+C
1143
- process.on('SIGINT', () => {
1139
+ const sigintHandler = () => {
1144
1140
  console.log(chalk_1.default.yellow('\nPairing cancelled.'));
1145
1141
  websocket_1.wsClient.disconnect();
1146
1142
  process.exit(0);
1143
+ };
1144
+ websocket_1.wsClient.once('pair_device', (data) => {
1145
+ process.removeListener('SIGINT', sigintHandler);
1146
+ resolve({ mobileDeviceId: data.mobileDeviceId });
1147
1147
  });
1148
+ // Handle Ctrl+C
1149
+ process.on('SIGINT', sigintHandler);
1148
1150
  });
1149
1151
  }
1150
1152
  // Logs command — list and manage debug log files
@@ -252,10 +252,10 @@ declare class ClaudeProcessManager extends EventEmitter {
252
252
  */
253
253
  getAllPendingPrompts(): PermissionPromptEvent[];
254
254
  /**
255
- * Auto-allow all pending permission prompts across all IPC managers.
256
- * Called when mobile disconnects so Claude doesn't hang waiting for approval.
255
+ * Resolve all pending permission prompts with a given decision.
256
+ * Called when mobile disconnects defaults to 'deny' since the user can't verify.
257
257
  */
258
- autoAllowAllPendingPrompts(): void;
258
+ resolveAllPendingPrompts(decision: 'allow' | 'deny', reason: string): void;
259
259
  /**
260
260
  * Tear down all permission hooks and IPC managers.
261
261
  * Called when mobile disconnects — hooks get re-configured on next Take Over + message.
@@ -318,10 +318,23 @@ class ClaudeProcessManager extends events_1.EventEmitter {
318
318
  // If there's an existing process, kill it first (Claude SDK only supports 1 turn per process)
319
319
  if (info?.process && info.process.exitCode === null) {
320
320
  console.log(`[Claude Process] Killing existing process for new message (SDK limitation: 1 turn per process)`);
321
- info.process.kill('SIGTERM');
322
- // Wait for process to die
323
- await new Promise(resolve => setTimeout(resolve, 200));
321
+ const oldProc = info.process;
324
322
  this.processes.delete(terminalSessionId);
323
+ oldProc.kill('SIGTERM');
324
+ // Wait for process to exit: SIGTERM first, escalate to SIGKILL after 1.5s
325
+ await new Promise(resolve => {
326
+ const escalate = setTimeout(() => { try {
327
+ oldProc.kill('SIGKILL');
328
+ }
329
+ catch { } }, 1500);
330
+ const timeout = setTimeout(() => { clearTimeout(escalate); resolve(); }, 3000);
331
+ oldProc.once('close', () => { clearTimeout(timeout); clearTimeout(escalate); resolve(); });
332
+ if (oldProc.exitCode !== null) {
333
+ clearTimeout(timeout);
334
+ clearTimeout(escalate);
335
+ resolve();
336
+ }
337
+ });
325
338
  info = undefined;
326
339
  }
327
340
  // Get session info from either current process or closed sessions
@@ -878,19 +891,18 @@ class ClaudeProcessManager extends events_1.EventEmitter {
878
891
  return allPrompts;
879
892
  }
880
893
  /**
881
- * Auto-allow all pending permission prompts across all IPC managers.
882
- * Called when mobile disconnects so Claude doesn't hang waiting for approval.
894
+ * Resolve all pending permission prompts with a given decision.
895
+ * Called when mobile disconnects defaults to 'deny' since the user can't verify.
883
896
  */
884
- autoAllowAllPendingPrompts() {
897
+ resolveAllPendingPrompts(decision, reason) {
885
898
  const pending = this.getAllPendingPrompts();
886
899
  for (const prompt of pending) {
887
- // Find the IPC manager that owns this prompt and respond
888
900
  for (const [, ipcManager] of this.permissionIpcManagers) {
889
- ipcManager.handleResponse(prompt.promptId, 'allow', 'Auto-allowed: mobile disconnected');
901
+ ipcManager.handleResponse(prompt.promptId, decision, reason);
890
902
  }
891
903
  }
892
904
  if (pending.length > 0) {
893
- console.log(`[Claude Process] Auto-allowed ${pending.length} pending permission prompt(s) on mobile disconnect`);
905
+ console.log(`[Claude Process] Auto-${decision === 'allow' ? 'allowed' : 'denied'} ${pending.length} pending permission prompt(s): ${reason}`);
894
906
  }
895
907
  }
896
908
  /**
package/dist/websocket.js CHANGED
@@ -32,6 +32,7 @@ const ALLOWED_ENCRYPTED_EVENTS = new Set([
32
32
  'sdk_session_history',
33
33
  'claude_abort',
34
34
  'usage_stats_request',
35
+ 'session_release',
35
36
  ]);
36
37
  // Events that carry user data and MUST be encrypted — plaintext fallback is refused.
37
38
  // If E2EE is not established, these are queued (not sent in plaintext).
@@ -156,10 +157,11 @@ class WebSocketClient extends events_1.EventEmitter {
156
157
  wireUpTransportEvents() {
157
158
  if (!this.server)
158
159
  return;
159
- // When mobile connects, emit connected + start heartbeat + initiate E2EE
160
+ // When mobile connects, emit connected + send immediate status + start heartbeat + initiate E2EE
160
161
  this.server.on('mobile_connected', (data) => {
161
162
  console.log(`[WS] Mobile connected: ${data.deviceId?.substring(0, 8)}...`);
162
163
  this.emit('connected');
164
+ this.sendHeartbeat(); // Immediate status update — don't wait for first 30s interval
163
165
  this.startHeartbeat();
164
166
  // SECURITY: Clear stale E2EE peer state on every mobile connect.
165
167
  // Mobile sessions are in-memory only — lost on app restart/reconnect.
@@ -474,10 +476,9 @@ class WebSocketClient extends events_1.EventEmitter {
474
476
  }
475
477
  /** SECURITY: Check if plaintext inbound events should be dropped (E2EE active with peer) */
476
478
  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
479
+ // Drop plaintext events when an E2EE session is established with the peer
480
+ // legitimate events should arrive as encrypted_message, not plaintext.
481
+ // Applies to both cloud relay and local modes.
481
482
  return !!(this.e2eePeerDeviceId && this.e2eeManager?.hasSessionKey(this.e2eePeerDeviceId));
482
483
  }
483
484
  disconnect() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forkoff",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "CLI tool to connect Claude Code to the ForkOff mobile app for remote monitoring and approvals",
5
5
  "main": "dist/integration.js",
6
6
  "types": "dist/integration.d.ts",