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.
- package/agent/agent.js +88 -7
- package/package.json +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
2146
|
-
|
|
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
|
-
//
|
|
2171
|
-
|
|
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(),
|