minivibe 0.2.10 → 0.2.11

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.
Files changed (3) hide show
  1. package/agent/agent.js +88 -7
  2. package/package.json +1 -1
  3. package/vibe.js +91 -5
package/agent/agent.js CHANGED
@@ -76,6 +76,40 @@ function log(msg, color = colors.reset) {
76
76
  console.log(`${colors.dim}[${timestamp}]${colors.reset} ${color}${msg}${colors.reset}`);
77
77
  }
78
78
 
79
+ /**
80
+ * Clean CLI output for logging - removes ANSI codes, TUI elements, and collapses whitespace
81
+ */
82
+ function cleanCliOutput(data) {
83
+ let output = data.toString();
84
+
85
+ // Strip all ANSI escape sequences (comprehensive pattern)
86
+ // Includes: CSI sequences, OSC sequences, DCS sequences, and simple escapes
87
+ output = output.replace(/\x1b\[[?]?[0-9;]*[a-zA-Z]/g, ''); // CSI with optional ?
88
+ output = output.replace(/\x1b\][^\x07]*\x07/g, ''); // OSC sequences
89
+ output = output.replace(/\x1b[PX^_].*?\x1b\\/g, ''); // DCS/PM/APC sequences
90
+ output = output.replace(/\x1b[()][AB012]/g, ''); // Character set selection
91
+ output = output.replace(/\x1b[78DEHM]/g, ''); // Other simple escapes
92
+
93
+ // Strip other control characters except newline and tab
94
+ output = output.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
95
+
96
+ // Filter out TUI status bar elements (Claude's UI)
97
+ output = output.replace(/[─│┌┐└┘├┤┬┴┼]+/g, ''); // Box drawing chars
98
+ output = output.replace(/[✻◐◑◒◓⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/g, ''); // Spinner chars
99
+ output = output.replace(/\?\s*for shortcuts.*toggle\)/gi, ''); // Status bar text
100
+ output = output.replace(/Thinking\s+(on|off)\s*\(tab to toggle\)/gi, '');
101
+ output = output.replace(/\(esc to interrupt\)/gi, '');
102
+ output = output.replace(/Envisioning…/g, '');
103
+
104
+ // Collapse multiple spaces/newlines
105
+ output = output.replace(/[ \t]+/g, ' ');
106
+ output = output.replace(/\n{2,}/g, '\n');
107
+
108
+ // Trim and return (return empty string if only whitespace remains)
109
+ output = output.trim();
110
+ return output.length > 0 ? output : '';
111
+ }
112
+
79
113
  // ====================
80
114
  // Configuration Management
81
115
  // ====================
@@ -188,6 +222,7 @@ let agentId = null;
188
222
  let hostName = os.hostname();
189
223
  let reconnectTimer = null;
190
224
  let heartbeatTimer = null;
225
+ let e2eEnabled = false; // E2E DISABLED - WSS provides transport encryption (use --e2e to enable)
191
226
 
192
227
  // Track running sessions: sessionId -> { process, path, name, localWs }
193
228
  const runningSessions = new Map();
@@ -759,6 +794,7 @@ function handleMessage(msg) {
759
794
  case 'agent_registered':
760
795
  log(`Agent registered: ${msg.agentId}`, colors.green);
761
796
  log(`Host: ${hostName}`, colors.dim);
797
+ log(`E2E encryption: ${e2eEnabled ? 'enabled' : 'disabled'}`, colors.dim);
762
798
  log('Waiting for commands...', colors.cyan);
763
799
  break;
764
800
 
@@ -790,6 +826,22 @@ function handleMessage(msg) {
790
826
  log(`Bridge error: ${msg.message}`, colors.red);
791
827
  // Relay errors to local client if applicable
792
828
  relayToLocalClient(msg);
829
+
830
+ // Check for auth-related errors and try to refresh token
831
+ const errorLower = (msg.message || '').toLowerCase();
832
+ if (errorLower.includes('token') || errorLower.includes('auth') || errorLower.includes('unauthorized')) {
833
+ log('Attempting token refresh...', colors.yellow);
834
+ (async () => {
835
+ const newToken = await refreshIdToken();
836
+ if (newToken) {
837
+ authToken = newToken;
838
+ log('Token refreshed, re-authenticating...', colors.green);
839
+ send({ type: 'authenticate', token: newToken });
840
+ } else {
841
+ log('Token refresh failed. Please re-login: vibe-agent login', colors.red);
842
+ }
843
+ })();
844
+ }
793
845
  break;
794
846
 
795
847
  // Messages to relay to local vibe-cli clients
@@ -808,6 +860,14 @@ function handleMessage(msg) {
808
860
  relayToLocalClient(msg);
809
861
  break;
810
862
 
863
+ case 'send_message':
864
+ // Mobile sent a message - relay to local vibe-cli
865
+ log(`📱 Relaying send_message to local client: "${(msg.content || '').toString().slice(0, 50)}..."`, colors.cyan);
866
+ if (!relayToLocalClient(msg)) {
867
+ log(`⚠️ Failed to relay send_message - no local client for session ${msg.sessionId?.slice(0, 8)}`, colors.yellow);
868
+ }
869
+ break;
870
+
811
871
  default:
812
872
  // Try to relay unknown messages to local client
813
873
  if (msg.sessionId) {
@@ -898,6 +958,11 @@ function handleStartSession(msg) {
898
958
  // Build args - use --agent to connect via local server
899
959
  const args = ['--agent', `ws://localhost:${LOCAL_SERVER_PORT}`];
900
960
 
961
+ // Pass E2E flag if enabled (needed to decrypt messages from iOS)
962
+ if (e2eEnabled) {
963
+ args.push('--e2e');
964
+ }
965
+
901
966
  if (name) {
902
967
  args.push('--name', name);
903
968
  }
@@ -945,15 +1010,15 @@ function handleStartSession(msg) {
945
1010
  });
946
1011
 
947
1012
  proc.stdout.on('data', (data) => {
948
- // Log output for debugging
949
- const output = data.toString().trim();
1013
+ // Log output for debugging (cleaned to remove ANSI codes and excess newlines)
1014
+ const output = cleanCliOutput(data);
950
1015
  if (output) {
951
1016
  log(`[${newSessionId.slice(0, 8)}] ${output}`, colors.dim);
952
1017
  }
953
1018
  });
954
1019
 
955
1020
  proc.stderr.on('data', (data) => {
956
- const output = data.toString().trim();
1021
+ const output = cleanCliOutput(data);
957
1022
  if (output) {
958
1023
  log(`[${newSessionId.slice(0, 8)}] ${output}`, colors.yellow);
959
1024
  }
@@ -1069,6 +1134,11 @@ function handleResumeSession(msg) {
1069
1134
  // Build args with --resume - use --agent to connect via local server
1070
1135
  const args = ['--agent', `ws://localhost:${LOCAL_SERVER_PORT}`, '--resume', sessionId];
1071
1136
 
1137
+ // Pass E2E flag if enabled (needed to decrypt messages from iOS)
1138
+ if (e2eEnabled) {
1139
+ args.push('--e2e');
1140
+ }
1141
+
1072
1142
  // Try to get session info from history if path not provided
1073
1143
  let effectivePath = projectPath;
1074
1144
  let effectiveName = name;
@@ -1134,14 +1204,14 @@ function handleResumeSession(msg) {
1134
1204
  });
1135
1205
 
1136
1206
  proc.stdout.on('data', (data) => {
1137
- const output = data.toString().trim();
1207
+ const output = cleanCliOutput(data);
1138
1208
  if (output) {
1139
1209
  log(`[${sessionId.slice(0, 8)}] ${output}`, colors.dim);
1140
1210
  }
1141
1211
  });
1142
1212
 
1143
1213
  proc.stderr.on('data', (data) => {
1144
- const output = data.toString().trim();
1214
+ const output = cleanCliOutput(data);
1145
1215
  if (output) {
1146
1216
  log(`[${sessionId.slice(0, 8)}] ${output}`, colors.yellow);
1147
1217
  }
@@ -1374,11 +1444,13 @@ ${colors.bold}Options:${colors.reset}
1374
1444
  --name <name> Set host display name
1375
1445
  --bridge <url> Override bridge URL (default: wss://ws.minivibeapp.com)
1376
1446
  --token <token> Use specific Firebase token
1447
+ --e2e Enable E2E encryption (disabled by default, WSS provides transport encryption)
1377
1448
 
1378
1449
  ${colors.bold}Examples:${colors.reset}
1379
1450
  vibe-agent login Sign in (one-time setup)
1380
- vibe-agent Start agent
1451
+ vibe-agent Start agent (E2E disabled, uses WSS transport encryption)
1381
1452
  vibe-agent --name "EC2" Start with custom name
1453
+ vibe-agent --e2e Enable E2E encryption
1382
1454
  `);
1383
1455
  }
1384
1456
 
@@ -1405,7 +1477,8 @@ function parseArgs() {
1405
1477
  logout: false,
1406
1478
  name: null,
1407
1479
  status: false,
1408
- help: false
1480
+ help: false,
1481
+ e2e: false // --e2e flag to enable E2E (E2E is OFF by default)
1409
1482
  };
1410
1483
 
1411
1484
  for (let i = 0; i < args.length; i++) {
@@ -1433,6 +1506,9 @@ function parseArgs() {
1433
1506
  case '-h':
1434
1507
  options.help = true;
1435
1508
  break;
1509
+ case '--e2e':
1510
+ options.e2e = true;
1511
+ break;
1436
1512
  }
1437
1513
  }
1438
1514
 
@@ -1457,6 +1533,8 @@ async function main() {
1457
1533
  bridgeUrl = options.bridge || config.bridgeUrl || DEFAULT_BRIDGE_URL;
1458
1534
  hostName = options.name || config.hostName || os.hostname();
1459
1535
  agentId = config.agentId || null; // Load persisted agentId
1536
+ // E2E is OFF by default (WSS provides transport encryption), enable with --e2e flag or config.e2e
1537
+ e2eEnabled = options.e2e || config.e2e || false;
1460
1538
 
1461
1539
  if (options.token) {
1462
1540
  authToken = options.token;
@@ -1473,6 +1551,9 @@ async function main() {
1473
1551
  if (options.name) {
1474
1552
  config.hostName = hostName;
1475
1553
  }
1554
+ if (options.e2e) {
1555
+ config.e2e = true; // Persist --e2e setting
1556
+ }
1476
1557
  saveConfig(config);
1477
1558
 
1478
1559
  // Status check
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minivibe",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "CLI wrapper for Claude Code with mobile remote control via MiniVibe iOS app",
5
5
  "author": "MiniVibe <hello@minivibeapp.com>",
6
6
  "homepage": "https://github.com/minivibeapp/minivibe",
package/vibe.js CHANGED
@@ -680,6 +680,12 @@ let lastCapturedPrompt = null; // Last permission prompt captured from CLI
680
680
  const mobileMessageHashes = new Set(); // Track messages from mobile to avoid duplicate echo
681
681
  const MAX_MOBILE_MESSAGES = 100; // Limit Set size
682
682
 
683
+ // Buffer for encrypted messages received before key exchange completes
684
+ // These will be processed after key exchange succeeds
685
+ const e2ePendingMessages = [];
686
+ const MAX_E2E_PENDING_MESSAGES = 10;
687
+ const E2E_PENDING_TIMEOUT_MS = 30000; // Discard buffered messages older than 30s
688
+
683
689
  // In-session slash command support
684
690
  let inputBuffer = ''; // Buffer for detecting slash commands
685
691
  const SLASH_COMMANDS = ['/name', '/upload', '/download', '/files', '/status', '/info', '/help'];
@@ -990,6 +996,39 @@ function handleBridgeMessage(msg) {
990
996
  }
991
997
  logStderr('[E2E] Encryption established!', colors.green);
992
998
  e2ePending = false;
999
+
1000
+ // Process any buffered messages that were waiting for key exchange
1001
+ if (e2ePendingMessages.length > 0) {
1002
+ logStderr(`[E2E] Processing ${e2ePendingMessages.length} buffered message(s)...`, colors.cyan);
1003
+ const now = Date.now();
1004
+ let failedCount = 0;
1005
+ while (e2ePendingMessages.length > 0) {
1006
+ const pending = e2ePendingMessages.shift();
1007
+ // Skip messages older than timeout
1008
+ if (now - pending.timestamp > E2E_PENDING_TIMEOUT_MS) {
1009
+ logStderr('[E2E] Discarding stale buffered message', colors.dim);
1010
+ continue;
1011
+ }
1012
+ // Retry decryption with fresh keys
1013
+ try {
1014
+ const decrypted = e2e.decryptContent(pending.msg);
1015
+ logStderr('[E2E] Successfully decrypted buffered message', colors.green);
1016
+ handleBridgeMessage(decrypted);
1017
+ } catch (err) {
1018
+ // Decryption still failed - message was encrypted with old keys
1019
+ failedCount++;
1020
+ logStderr(`[E2E] Buffered message encrypted with old keys - cannot decrypt`, colors.yellow);
1021
+ }
1022
+ }
1023
+ // Warn user about failed messages - they need to re-send from iOS
1024
+ if (failedCount > 0) {
1025
+ logStderr(``, colors.yellow);
1026
+ logStderr(`⚠️ ${failedCount} message(s) could not be decrypted`, colors.yellow);
1027
+ logStderr(` These were encrypted before CLI received your new keys.`, colors.dim);
1028
+ logStderr(` Please re-send from iOS.`, colors.dim);
1029
+ logStderr(``, colors.yellow);
1030
+ }
1031
+ }
993
1032
  }
994
1033
  return;
995
1034
  }
@@ -1118,11 +1157,40 @@ function handleBridgeMessage(msg) {
1118
1157
  case 'send_message':
1119
1158
  // Mobile sent a message - forward to Claude
1120
1159
  // Track it so we don't echo it back (bridge already echoed to iOS)
1160
+ {
1161
+ const contentPreview = typeof msg.content === 'string'
1162
+ ? msg.content.slice(0, 100)
1163
+ : (msg.content ? JSON.stringify(msg.content).slice(0, 100) : '(empty)');
1164
+ log(`📱 Received send_message from ${msg.source || 'unknown'}: "${contentPreview}..."`, colors.cyan);
1165
+ }
1121
1166
  if (msg.content) {
1122
1167
  // Check if decryption failed (content is still an encrypted object)
1123
1168
  if (typeof msg.content === 'object' && msg.content.e2e) {
1169
+ // If E2E is enabled but key exchange hasn't completed yet, buffer the message
1170
+ if (e2eEnabled && !e2e.isReady()) {
1171
+ if (e2ePendingMessages.length < MAX_E2E_PENDING_MESSAGES) {
1172
+ logStderr('[E2E] Key exchange not complete - buffering encrypted message...', colors.cyan);
1173
+ e2ePendingMessages.push({ msg: { ...msg }, timestamp: Date.now() });
1174
+ // Notify iOS that message is pending
1175
+ sendToBridge({
1176
+ type: 'message_pending',
1177
+ sessionId: effectiveSessionId,
1178
+ reason: 'Waiting for E2E key exchange to complete'
1179
+ });
1180
+ } else {
1181
+ logStderr('[E2E] Buffer full - discarding encrypted message', colors.yellow);
1182
+ }
1183
+ break;
1184
+ }
1185
+ // Key exchange completed but still can't decrypt - keys are out of sync
1124
1186
  logStderr('⚠️ Failed to decrypt message from mobile - E2E keys may be out of sync', colors.yellow);
1125
1187
  logStderr(' Try resetting E2E on both CLI (delete ~/.vibe/e2e-keys.json) and iOS', colors.dim);
1188
+ // Send error back to iOS so user knows message failed
1189
+ sendToBridge({
1190
+ type: 'error',
1191
+ sessionId: effectiveSessionId,
1192
+ message: 'E2E encryption mismatch - CLI cannot decrypt message. Disable E2E on iOS or restart CLI with --e2e flag.'
1193
+ });
1126
1194
  break;
1127
1195
  }
1128
1196
 
@@ -1141,7 +1209,18 @@ function handleBridgeMessage(msg) {
1141
1209
  mobileMessageHashes.delete(first);
1142
1210
  }
1143
1211
  mobileMessageHashes.add(hash);
1144
- sendToClaude(contentStr, msg.source || 'mobile');
1212
+ log(`📤 Sending to Claude: "${contentStr.slice(0, 100)}..."`, colors.dim);
1213
+ if (!sendToClaude(contentStr, msg.source || 'mobile')) {
1214
+ log('⚠️ Failed to send message to Claude (not running?)', colors.yellow);
1215
+ // Notify bridge of failure
1216
+ sendToBridge({
1217
+ type: 'error',
1218
+ sessionId: effectiveSessionId,
1219
+ message: 'Claude is not running - message not delivered'
1220
+ });
1221
+ }
1222
+ } else {
1223
+ log('⚠️ send_message received with no content', colors.yellow);
1145
1224
  }
1146
1225
  break;
1147
1226
 
@@ -2142,8 +2221,12 @@ function startClaude() {
2142
2221
  process.exit(1);
2143
2222
  }
2144
2223
 
2145
- // Always use FD 4 for terminal output mirroring (needed for proper PTY handling)
2146
- const stdioConfig = ['pipe', 'inherit', 'inherit', 'pipe', 'pipe'];
2224
+ // FD 4 is for terminal output mirroring (needed for proper PTY handling)
2225
+ // In agent mode, don't inherit stdout/stderr - Claude's TUI would flood agent console
2226
+ // Terminal output is forwarded via FD 4 anyway
2227
+ const stdioConfig = agentUrl
2228
+ ? ['pipe', 'ignore', 'ignore', 'pipe', 'pipe']
2229
+ : ['pipe', 'inherit', 'inherit', 'pipe', 'pipe'];
2147
2230
 
2148
2231
  claudeProcess = spawn('node', [nodeWrapperPath, claudePath, ...claudeArgs], {
2149
2232
  cwd: process.cwd(),
@@ -2167,8 +2250,11 @@ function startClaude() {
2167
2250
  process.exit(1);
2168
2251
  }
2169
2252
 
2170
- // Always use FD 4 for terminal output mirroring (needed for proper PTY handling)
2171
- const stdioConfig = ['pipe', 'inherit', 'inherit', 'pipe', 'pipe'];
2253
+ // FD 4 is for terminal output mirroring (needed for proper PTY handling)
2254
+ // In agent mode, don't inherit stdout/stderr - Claude's TUI would flood agent console
2255
+ const stdioConfig = agentUrl
2256
+ ? ['pipe', 'ignore', 'ignore', 'pipe', 'pipe']
2257
+ : ['pipe', 'inherit', 'inherit', 'pipe', 'pipe'];
2172
2258
 
2173
2259
  claudeProcess = spawn('python3', ['-u', pythonWrapperPath, claudePath, ...claudeArgs], {
2174
2260
  cwd: process.cwd(),