minivibe 0.1.4 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2025 MiniVibe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -11,6 +11,7 @@ CLI wrapper for Claude Code with mobile remote control via MiniVibe iOS app.
11
11
  - Token usage tracking
12
12
  - Headless authentication for servers (EC2, etc.)
13
13
  - Skip permissions mode for automation
14
+ - **End-to-end encryption** - Optional E2E encryption so bridge server cannot read message content
14
15
 
15
16
  ## Quick Start
16
17
 
@@ -23,15 +24,13 @@ vibe --login # Desktop (opens browser)
23
24
  vibe --login --headless # Server/EC2 (device code)
24
25
 
25
26
  # Option 1: Direct bridge connection
26
- vibe --bridge wss://ws.neng.ai
27
+ vibe --bridge wss://ws.minivibeapp.com
27
28
 
28
29
  # Option 2: Agent mode (recommended for servers)
29
- vibe-agent --bridge wss://ws.neng.ai & # Start agent
30
+ vibe-agent --bridge wss://ws.minivibeapp.com & # Start agent
30
31
  vibe --agent # Create sessions
31
32
  ```
32
33
 
33
- See [GETTING_STARTED.md](GETTING_STARTED.md) for detailed setup instructions.
34
-
35
34
  ## Installation
36
35
 
37
36
  ### From npm (Recommended)
@@ -47,8 +46,8 @@ This installs two commands:
47
46
  ### From Source
48
47
 
49
48
  ```bash
50
- git clone https://github.com/python3isfun/neng.git
51
- cd neng/vibe-cli
49
+ git clone https://github.com/minivibeapp/minivibe.git
50
+ cd minivibe
52
51
  npm install
53
52
  npm link
54
53
  ```
@@ -86,8 +85,8 @@ Get token from MiniVibe iOS app: Settings > Copy Token for CLI.
86
85
  Connect directly to the bridge server:
87
86
 
88
87
  ```bash
89
- vibe --bridge wss://ws.neng.ai
90
- vibe --bridge wss://ws.neng.ai "Fix the bug in main.js"
88
+ vibe --bridge wss://ws.minivibeapp.com
89
+ vibe --bridge wss://ws.minivibeapp.com "Fix the bug in main.js"
91
90
  ```
92
91
 
93
92
  ### Agent Mode (Recommended for Servers)
@@ -96,7 +95,7 @@ Use a local agent to manage sessions:
96
95
 
97
96
  ```bash
98
97
  # Terminal 1: Start the agent (runs continuously)
99
- vibe-agent --bridge wss://ws.neng.ai
98
+ vibe-agent --bridge wss://ws.minivibeapp.com
100
99
 
101
100
  # Terminal 2+: Create sessions via agent
102
101
  vibe --agent
@@ -128,12 +127,16 @@ vibe "Explain this code" # With prompt
128
127
  | `--bridge <url>` | Connect to bridge server |
129
128
  | `--agent [url]` | Connect via local vibe-agent (default: auto-discover) |
130
129
  | `--name <name>` | Name this session (shown in mobile app) |
131
- | `--resume <id>` | Resume a previous session |
130
+ | `--resume <id>` | Resume a previous session (auto-detects directory) |
131
+ | `--attach <id>` | Attach to running session via local agent |
132
+ | `--remote <id>` | Remote control session via bridge (no local Claude needed) |
133
+ | `--list` | List running sessions on local agent |
132
134
  | `--login` | Sign in with Google |
133
135
  | `--headless` | Use device code flow for headless environments |
134
136
  | `--token <token>` | Set Firebase auth token manually |
135
137
  | `--logout` | Remove stored auth token |
136
138
  | `--dangerously-skip-permissions` | Auto-approve all tool executions |
139
+ | `--e2e` | Enable end-to-end encryption (auto key exchange with iOS) |
137
140
  | `--node-pty` | Use Node.js PTY wrapper (required for Windows) |
138
141
  | `--help, -h` | Show help message |
139
142
 
@@ -142,7 +145,10 @@ vibe "Explain this code" # With prompt
142
145
  | Option | Description |
143
146
  |--------|-------------|
144
147
  | `--bridge <url>` | Bridge server URL (required) |
145
- | `--port <port>` | Local WebSocket port (default: 9999) |
148
+ | `--login` | Start device code login flow |
149
+ | `--token <token>` | Use specific Firebase token |
150
+ | `--name <name>` | Set host display name |
151
+ | `--status` | Show current status and exit |
146
152
  | `--help, -h` | Show help message |
147
153
 
148
154
  ## Skip Permissions Mode
@@ -150,12 +156,48 @@ vibe "Explain this code" # With prompt
150
156
  For automated/headless environments where you trust the execution context:
151
157
 
152
158
  ```bash
153
- vibe --dangerously-skip-permissions --bridge wss://ws.neng.ai
159
+ vibe --dangerously-skip-permissions --bridge wss://ws.minivibeapp.com
154
160
  vibe --dangerously-skip-permissions --agent
155
161
  ```
156
162
 
157
163
  **Warning:** This mode auto-approves ALL tool executions (commands, file writes, etc.) without prompting. Only use in trusted/sandboxed environments.
158
164
 
165
+ ## End-to-End Encryption
166
+
167
+ Enable E2E encryption to ensure the bridge server cannot read your message content:
168
+
169
+ ```bash
170
+ # Start with E2E encryption enabled
171
+ vibe --e2e --bridge wss://ws.minivibeapp.com
172
+ ```
173
+
174
+ Key exchange happens automatically when both CLI and iOS connect to the bridge:
175
+
176
+ 1. Enable E2E in MiniVibe iOS app: **Settings > Security > E2E Encryption**
177
+ 2. Start CLI with `--e2e` flag
178
+ 3. Both sides exchange public keys automatically on connect
179
+ 4. Encryption is established - no QR scanning needed!
180
+
181
+ ### How It Works
182
+
183
+ - Uses **X25519** key exchange (same as Signal, WhatsApp)
184
+ - Messages encrypted with **AES-256-GCM**
185
+ - Keys derived using **HKDF-SHA256**
186
+ - Bridge server sees message routing info but cannot read content
187
+
188
+ ### Key Storage
189
+
190
+ | Location | Description |
191
+ |----------|-------------|
192
+ | `~/.vibe/e2e-keys.json` | CLI keypair and peer info |
193
+ | iOS Keychain | iOS keypair and peer info |
194
+
195
+ ### Security Notes
196
+
197
+ - E2E is optional and backward compatible
198
+ - Once paired, encryption persists across sessions
199
+ - To re-pair: delete `~/.vibe/e2e-keys.json` and reset in iOS Settings
200
+
159
201
  ## Architecture
160
202
 
161
203
  ```
@@ -197,6 +239,7 @@ May also need Visual Studio Build Tools and Python for native compilation.
197
239
  |------|-------------|
198
240
  | `~/.vibe/auth.json` | Stored authentication (token + refresh token) |
199
241
  | `~/.vibe/token` | Legacy token file |
242
+ | `~/.vibe/e2e-keys.json` | E2E encryption keypair and peer info |
200
243
  | `~/.vibe-agent/port` | Agent port file for auto-discovery |
201
244
 
202
245
  ## License
package/agent/agent.js CHANGED
@@ -9,8 +9,8 @@
9
9
  * - Stop running sessions
10
10
  *
11
11
  * Usage:
12
- * vibe-agent --bridge wss://ws.neng.ai --token <firebase-token>
13
- * vibe-agent --login --bridge wss://ws.neng.ai
12
+ * vibe-agent --bridge wss://ws.minivibeapp.com --token <firebase-token>
13
+ * vibe-agent --login --bridge wss://ws.minivibeapp.com
14
14
  */
15
15
 
16
16
  const { spawn, execSync } = require('child_process');
@@ -28,11 +28,13 @@ const os = require('os');
28
28
  const CONFIG_DIR = path.join(os.homedir(), '.vibe-agent');
29
29
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
30
30
  const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
31
+ const SESSION_HISTORY_FILE = path.join(CONFIG_DIR, 'session-history.json');
31
32
 
32
33
  const RECONNECT_DELAY_MS = 5000;
33
34
  const HEARTBEAT_INTERVAL_MS = 30000;
34
35
  const LOCAL_SERVER_PORT = 9999;
35
36
  const PORT_FILE = path.join(os.homedir(), '.vibe-agent', 'port');
37
+ const MAX_SESSION_HISTORY_AGE_DAYS = 30;
36
38
 
37
39
  // Colors for terminal output
38
40
  const colors = {
@@ -166,9 +168,53 @@ let heartbeatTimer = null;
166
168
  // Track running sessions: sessionId -> { process, path, name, localWs }
167
169
  const runningSessions = new Map();
168
170
 
171
+ // Track session history for resume: sessionId -> { path, name, endedAt }
172
+ // Kept even after session ends so we can resume with correct path
173
+ // Persisted to disk so it survives agent restarts
174
+ const MAX_SESSION_HISTORY = 100;
175
+ const sessionHistory = loadSessionHistoryFromDisk();
176
+
169
177
  // Track sessions being intentionally stopped (to distinguish from unexpected disconnects)
170
178
  const stoppingSessions = new Set();
171
179
 
180
+ // Load session history from disk on startup
181
+ function loadSessionHistoryFromDisk() {
182
+ try {
183
+ if (fs.existsSync(SESSION_HISTORY_FILE)) {
184
+ const data = JSON.parse(fs.readFileSync(SESSION_HISTORY_FILE, 'utf8'));
185
+ const map = new Map();
186
+ const cutoffTime = Date.now() - (MAX_SESSION_HISTORY_AGE_DAYS * 24 * 60 * 60 * 1000);
187
+
188
+ // Filter out entries older than MAX_SESSION_HISTORY_AGE_DAYS
189
+ for (const [sessionId, info] of Object.entries(data)) {
190
+ if (new Date(info.endedAt).getTime() >= cutoffTime) {
191
+ map.set(sessionId, info);
192
+ }
193
+ }
194
+
195
+ console.log(`[vibe-agent] Loaded ${map.size} sessions from history`);
196
+ return map;
197
+ }
198
+ } catch (err) {
199
+ console.log(`[vibe-agent] Failed to load session history: ${err.message}`);
200
+ }
201
+ return new Map();
202
+ }
203
+
204
+ // Save session history to disk
205
+ function saveSessionHistoryToDisk() {
206
+ try {
207
+ // Ensure config directory exists
208
+ if (!fs.existsSync(CONFIG_DIR)) {
209
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
210
+ }
211
+ const data = Object.fromEntries(sessionHistory);
212
+ fs.writeFileSync(SESSION_HISTORY_FILE, JSON.stringify(data, null, 2));
213
+ } catch (err) {
214
+ console.log(`[vibe-agent] Failed to save session history: ${err.message}`);
215
+ }
216
+ }
217
+
172
218
  // Local server for vibe-cli connections
173
219
  let localServer = null;
174
220
  // Track local CLI connections: ws -> { sessionId, authenticated }
@@ -310,18 +356,48 @@ function startLocalServer() {
310
356
  const clientInfo = localClients.get(clientWs);
311
357
  if (clientInfo?.sessionId) {
312
358
  const sessionId = clientInfo.sessionId;
313
- const wasIntentionalStop = stoppingSessions.has(sessionId);
314
- stoppingSessions.delete(sessionId); // Clean up
315
-
316
- log(`Local session ${sessionId.slice(0, 8)} ${wasIntentionalStop ? 'stopped' : 'disconnected'}`, colors.dim);
317
- runningSessions.delete(sessionId);
318
- // Notify bridge
319
- send({
320
- type: 'agent_session_ended',
321
- sessionId: sessionId,
322
- exitCode: 0,
323
- reason: wasIntentionalStop ? 'stopped_by_user' : 'disconnected'
324
- });
359
+
360
+ // Check if this is an attached client (not the session owner)
361
+ if (clientInfo.isAttached) {
362
+ const session = runningSessions.get(sessionId);
363
+ if (session?.attachedClients) {
364
+ session.attachedClients.delete(clientWs);
365
+ log(`Attached client disconnected from session ${sessionId.slice(0, 8)}`, colors.dim);
366
+ }
367
+ } else {
368
+ // This is the session owner - end the session
369
+ const wasIntentionalStop = stoppingSessions.has(sessionId);
370
+ stoppingSessions.delete(sessionId); // Clean up
371
+
372
+ log(`Local session ${sessionId.slice(0, 8)} ${wasIntentionalStop ? 'stopped' : 'disconnected'}`, colors.dim);
373
+ // Save to history before deleting for resume capability
374
+ const session = runningSessions.get(sessionId);
375
+ if (session) {
376
+ saveSessionHistory(sessionId, session.path, session.name);
377
+ // Notify attached clients that session ended
378
+ if (session.attachedClients) {
379
+ for (const attachedWs of session.attachedClients) {
380
+ try {
381
+ attachedWs.send(JSON.stringify({
382
+ type: 'session_ended',
383
+ sessionId,
384
+ reason: wasIntentionalStop ? 'stopped_by_user' : 'disconnected'
385
+ }));
386
+ } catch (err) {
387
+ // Ignore
388
+ }
389
+ }
390
+ }
391
+ }
392
+ runningSessions.delete(sessionId);
393
+ // Notify bridge
394
+ send({
395
+ type: 'agent_session_ended',
396
+ sessionId: sessionId,
397
+ exitCode: 0,
398
+ reason: wasIntentionalStop ? 'stopped_by_user' : 'disconnected'
399
+ });
400
+ }
325
401
  }
326
402
  localClients.delete(clientWs);
327
403
  });
@@ -385,7 +461,8 @@ function handleLocalMessage(clientWs, msg) {
385
461
  path: msg.path || process.cwd(),
386
462
  name: msg.name || path.basename(msg.path || process.cwd()),
387
463
  startedAt: new Date().toISOString(),
388
- managed: true // Indicates connected via local server, not spawned
464
+ managed: true, // Indicates connected via local server, not spawned
465
+ attachedClients: new Set() // Track attached terminal clients
389
466
  });
390
467
 
391
468
  log(`Local session registered: ${sessionId.slice(0, 8)} (${msg.name || msg.path})`, colors.green);
@@ -411,13 +488,135 @@ function handleLocalMessage(clientWs, msg) {
411
488
  }
412
489
  break;
413
490
 
414
- // All other messages: relay to bridge
491
+ // Attach to existing session (terminal mirroring)
492
+ case 'attach_session':
493
+ const attachSessionId = msg.sessionId;
494
+
495
+ // Validate sessionId
496
+ if (!attachSessionId) {
497
+ clientWs.send(JSON.stringify({
498
+ type: 'attach_error',
499
+ error: 'sessionId is required'
500
+ }));
501
+ break;
502
+ }
503
+
504
+ // Check if session is stopping
505
+ if (stoppingSessions.has(attachSessionId)) {
506
+ log(`Attach failed: session ${attachSessionId.slice(0, 8)} is stopping`, colors.yellow);
507
+ clientWs.send(JSON.stringify({
508
+ type: 'attach_error',
509
+ sessionId: attachSessionId,
510
+ error: 'Session is currently stopping. Cannot attach.'
511
+ }));
512
+ break;
513
+ }
514
+
515
+ const session = runningSessions.get(attachSessionId);
516
+
517
+ if (!session) {
518
+ log(`Attach failed: session ${attachSessionId.slice(0, 8)} not found`, colors.red);
519
+ clientWs.send(JSON.stringify({
520
+ type: 'attach_error',
521
+ sessionId: attachSessionId,
522
+ error: 'Session not found. It may have ended or is running on a different agent.'
523
+ }));
524
+ break;
525
+ }
526
+
527
+ // Mark this client as attached to the session
528
+ clientInfo.sessionId = attachSessionId;
529
+ clientInfo.isAttached = true;
530
+
531
+ // Add to session's attached clients
532
+ if (!session.attachedClients) {
533
+ session.attachedClients = new Set();
534
+ }
535
+ session.attachedClients.add(clientWs);
536
+
537
+ log(`Client attached to session ${attachSessionId.slice(0, 8)} (${session.attachedClients.size} attached)`, colors.cyan);
538
+
539
+ clientWs.send(JSON.stringify({
540
+ type: 'attach_success',
541
+ sessionId: attachSessionId,
542
+ name: session.name,
543
+ path: session.path
544
+ }));
545
+ break;
546
+
547
+ // List running sessions
548
+ case 'list_sessions':
549
+ const sessionsList = [];
550
+ for (const [sessionId, session] of runningSessions) {
551
+ // Determine source: spawned sessions have 'process', managed have 'localWs'
552
+ // Sessions started from iOS come via bridge with spawn, sessions from CLI use --agent
553
+ const source = session.process ? 'ios' : 'cli';
554
+ sessionsList.push({
555
+ sessionId,
556
+ name: session.name,
557
+ path: session.path,
558
+ startedAt: session.startedAt,
559
+ source
560
+ });
561
+ }
562
+ clientWs.send(JSON.stringify({
563
+ type: 'sessions_list',
564
+ sessions: sessionsList
565
+ }));
566
+ log(`Listed ${sessionsList.length} running sessions`, colors.dim);
567
+ break;
568
+
569
+ // Terminal input from attached client - forward to session provider
570
+ case 'terminal_input':
571
+ const targetSession = runningSessions.get(clientInfo.sessionId);
572
+ if (targetSession && targetSession.localWs && clientInfo.isAttached) {
573
+ // Forward input to the session's provider (the original vibe-cli)
574
+ try {
575
+ targetSession.localWs.send(JSON.stringify({
576
+ type: 'terminal_input',
577
+ data: msg.data
578
+ }));
579
+ } catch (err) {
580
+ log(`Failed to forward terminal input: ${err.message}`, colors.red);
581
+ }
582
+ }
583
+ break;
584
+
585
+ // Terminal output from session provider - relay to attached clients
586
+ case 'terminal_output':
587
+ const outputSession = runningSessions.get(clientInfo.sessionId);
588
+ if (outputSession?.attachedClients) {
589
+ for (const attachedWs of outputSession.attachedClients) {
590
+ try {
591
+ attachedWs.send(JSON.stringify(msg));
592
+ } catch (err) {
593
+ // Ignore send errors
594
+ }
595
+ }
596
+ }
597
+ break;
598
+
599
+ // All other messages: relay to bridge and also to attached clients
415
600
  default:
416
601
  // Add sessionId if not present
417
602
  if (!msg.sessionId && clientInfo.sessionId) {
418
603
  msg.sessionId = clientInfo.sessionId;
419
604
  }
420
605
 
606
+ // Relay certain message types to attached clients for terminal mirroring
607
+ if (['claude_message', 'permission_request', 'session_status'].includes(msg.type)) {
608
+ const msgSession = runningSessions.get(clientInfo.sessionId);
609
+ if (msgSession?.attachedClients) {
610
+ for (const attachedWs of msgSession.attachedClients) {
611
+ try {
612
+ attachedWs.send(JSON.stringify(msg));
613
+ } catch (err) {
614
+ // Ignore send errors
615
+ }
616
+ }
617
+ }
618
+ }
619
+
421
620
  if (send(msg)) {
422
621
  // Message relayed successfully
423
622
  } else {
@@ -613,6 +812,32 @@ function findVibeCli() {
613
812
  }
614
813
  }
615
814
 
815
+ // Save session info to history for resume capability
816
+ function saveSessionHistory(sessionId, sessionPath, sessionName) {
817
+ // Limit history size
818
+ if (sessionHistory.size >= MAX_SESSION_HISTORY) {
819
+ // Delete oldest entry
820
+ const oldest = sessionHistory.keys().next().value;
821
+ sessionHistory.delete(oldest);
822
+ }
823
+
824
+ sessionHistory.set(sessionId, {
825
+ path: sessionPath,
826
+ name: sessionName,
827
+ endedAt: new Date().toISOString()
828
+ });
829
+
830
+ // Persist to disk
831
+ saveSessionHistoryToDisk();
832
+
833
+ log(`Saved session ${sessionId.slice(0, 8)} to history (path: ${sessionPath})`, colors.dim);
834
+ }
835
+
836
+ // Get session info from history
837
+ function getSessionFromHistory(sessionId) {
838
+ return sessionHistory.get(sessionId);
839
+ }
840
+
616
841
  function handleStartSession(msg) {
617
842
  const { sessionId, path: projectPath, name, prompt, requestId } = msg;
618
843
 
@@ -675,7 +900,8 @@ function handleStartSession(msg) {
675
900
  process: proc,
676
901
  path: cwd,
677
902
  name: name || path.basename(cwd),
678
- startedAt: new Date().toISOString()
903
+ startedAt: new Date().toISOString(),
904
+ attachedClients: new Set() // Support terminal attachment
679
905
  });
680
906
 
681
907
  proc.stdout.on('data', (data) => {
@@ -695,6 +921,11 @@ function handleStartSession(msg) {
695
921
 
696
922
  proc.on('exit', (code) => {
697
923
  log(`Session ${newSessionId.slice(0, 8)} exited with code ${code}`, colors.dim);
924
+ // Save to history before deleting for resume capability
925
+ const session = runningSessions.get(newSessionId);
926
+ if (session) {
927
+ saveSessionHistory(newSessionId, session.path, session.name);
928
+ }
698
929
  runningSessions.delete(newSessionId);
699
930
 
700
931
  send({
@@ -706,6 +937,11 @@ function handleStartSession(msg) {
706
937
 
707
938
  proc.on('error', (err) => {
708
939
  log(`Session error: ${err.message}`, colors.red);
940
+ // Save to history before deleting for resume capability
941
+ const session = runningSessions.get(newSessionId);
942
+ if (session) {
943
+ saveSessionHistory(newSessionId, session.path, session.name);
944
+ }
709
945
  runningSessions.delete(newSessionId);
710
946
 
711
947
  send({
@@ -741,6 +977,17 @@ function handleStartSession(msg) {
741
977
  function handleResumeSession(msg) {
742
978
  const { sessionId, path: projectPath, name, requestId } = msg;
743
979
 
980
+ // Validate sessionId is present
981
+ if (!sessionId) {
982
+ log('Resume session called without sessionId', colors.red);
983
+ send({
984
+ type: 'agent_session_error',
985
+ requestId,
986
+ error: 'sessionId is required to resume a session'
987
+ });
988
+ return;
989
+ }
990
+
744
991
  log(`Resuming session: ${sessionId.slice(0, 8)}`, colors.cyan);
745
992
 
746
993
  // Check if already running
@@ -755,6 +1002,18 @@ function handleResumeSession(msg) {
755
1002
  return;
756
1003
  }
757
1004
 
1005
+ // Check if session is currently being stopped
1006
+ if (stoppingSessions.has(sessionId)) {
1007
+ log('Session is currently stopping', colors.yellow);
1008
+ send({
1009
+ type: 'agent_session_error',
1010
+ requestId,
1011
+ sessionId,
1012
+ error: 'Session is currently stopping. Please wait a moment and try again.'
1013
+ });
1014
+ return;
1015
+ }
1016
+
758
1017
  const vibeCli = findVibeCli();
759
1018
  if (!vibeCli) {
760
1019
  log('vibe-cli not found!', colors.red);
@@ -770,12 +1029,37 @@ function handleResumeSession(msg) {
770
1029
  // Build args with --resume - use --agent to connect via local server
771
1030
  const args = ['--agent', `ws://localhost:${LOCAL_SERVER_PORT}`, '--resume', sessionId];
772
1031
 
773
- if (name) {
774
- args.push('--name', name);
1032
+ // Try to get session info from history if path not provided
1033
+ let effectivePath = projectPath;
1034
+ let effectiveName = name;
1035
+
1036
+ if (!effectivePath) {
1037
+ const historyEntry = getSessionFromHistory(sessionId);
1038
+ if (historyEntry) {
1039
+ effectivePath = historyEntry.path;
1040
+ effectiveName = effectiveName || historyEntry.name;
1041
+ log(`Using path from session history: ${effectivePath}`, colors.dim);
1042
+ }
1043
+ }
1044
+
1045
+ // Don't silently fall back to home directory - require a valid path
1046
+ if (!effectivePath) {
1047
+ log(`Cannot resume session ${sessionId.slice(0, 8)}: path unknown`, colors.red);
1048
+ send({
1049
+ type: 'agent_session_error',
1050
+ requestId,
1051
+ sessionId,
1052
+ error: 'Cannot resume session: working directory path unknown. The session may have been created before path tracking was enabled, or the agent was restarted.'
1053
+ });
1054
+ return;
1055
+ }
1056
+
1057
+ if (effectiveName) {
1058
+ args.push('--name', effectiveName);
775
1059
  }
776
1060
 
777
1061
  // Spawn vibe-cli with resume - expand ~ to home directory
778
- let cwd = projectPath || os.homedir();
1062
+ let cwd = effectivePath;
779
1063
  if (cwd.startsWith('~')) {
780
1064
  cwd = cwd.replace(/^~/, os.homedir());
781
1065
  }
@@ -804,8 +1088,9 @@ function handleResumeSession(msg) {
804
1088
  runningSessions.set(sessionId, {
805
1089
  process: proc,
806
1090
  path: cwd,
807
- name: name || path.basename(cwd),
808
- startedAt: new Date().toISOString()
1091
+ name: effectiveName || path.basename(cwd),
1092
+ startedAt: new Date().toISOString(),
1093
+ attachedClients: new Set() // Support terminal attachment
809
1094
  });
810
1095
 
811
1096
  proc.stdout.on('data', (data) => {
@@ -824,6 +1109,11 @@ function handleResumeSession(msg) {
824
1109
 
825
1110
  proc.on('exit', (code) => {
826
1111
  log(`Session ${sessionId.slice(0, 8)} exited with code ${code}`, colors.dim);
1112
+ // Save to history before deleting for resume capability
1113
+ const session = runningSessions.get(sessionId);
1114
+ if (session) {
1115
+ saveSessionHistory(sessionId, session.path, session.name);
1116
+ }
827
1117
  runningSessions.delete(sessionId);
828
1118
 
829
1119
  send({
@@ -835,6 +1125,11 @@ function handleResumeSession(msg) {
835
1125
 
836
1126
  proc.on('error', (err) => {
837
1127
  log(`Session error: ${err.message}`, colors.red);
1128
+ // Save to history before deleting for resume capability
1129
+ const session = runningSessions.get(sessionId);
1130
+ if (session) {
1131
+ saveSessionHistory(sessionId, session.path, session.name);
1132
+ }
838
1133
  runningSessions.delete(sessionId);
839
1134
 
840
1135
  send({
@@ -851,7 +1146,7 @@ function handleResumeSession(msg) {
851
1146
  requestId,
852
1147
  sessionId,
853
1148
  path: cwd,
854
- name: name || path.basename(cwd)
1149
+ name: effectiveName || path.basename(cwd)
855
1150
  });
856
1151
 
857
1152
  log(`Session resumed: ${sessionId.slice(0, 8)}`, colors.green);
@@ -920,6 +1215,10 @@ function handleStopSession(msg) {
920
1215
  } catch (err) {
921
1216
  // Already closed
922
1217
  }
1218
+ // Cleanup stoppingSessions as backup in case websocket close event doesn't fire
1219
+ setTimeout(() => {
1220
+ stoppingSessions.delete(sessionId);
1221
+ }, 5000);
923
1222
  }, 100);
924
1223
  runningSessions.delete(sessionId);
925
1224
  }
@@ -1025,7 +1324,7 @@ ${colors.bold}Usage:${colors.reset}
1025
1324
  vibe-agent --status Show agent status
1026
1325
 
1027
1326
  ${colors.bold}Options:${colors.reset}
1028
- --bridge <url> Bridge server URL (wss://ws.neng.ai)
1327
+ --bridge <url> Bridge server URL (wss://ws.minivibeapp.com)
1029
1328
  --login Start device code login flow
1030
1329
  --token <token> Use specific Firebase token
1031
1330
  --name <name> Set host display name
@@ -1034,13 +1333,13 @@ ${colors.bold}Options:${colors.reset}
1034
1333
 
1035
1334
  ${colors.bold}Examples:${colors.reset}
1036
1335
  # Login (first time)
1037
- vibe-agent --login --bridge wss://ws.neng.ai
1336
+ vibe-agent --login --bridge wss://ws.minivibeapp.com
1038
1337
 
1039
1338
  # Start agent daemon
1040
- vibe-agent --bridge wss://ws.neng.ai
1339
+ vibe-agent --bridge wss://ws.minivibeapp.com
1041
1340
 
1042
1341
  # Start with custom host name
1043
- vibe-agent --bridge wss://ws.neng.ai --name "AWS Dev Server"
1342
+ vibe-agent --bridge wss://ws.minivibeapp.com --name "AWS Dev Server"
1044
1343
  `);
1045
1344
  }
1046
1345
 
@@ -1141,7 +1440,7 @@ async function main() {
1141
1440
 
1142
1441
  // Validate requirements
1143
1442
  if (!bridgeUrl) {
1144
- log('No bridge URL. Run: vibe-agent --bridge wss://ws.neng.ai', colors.red);
1443
+ log('No bridge URL. Run: vibe-agent --bridge wss://ws.minivibeapp.com', colors.red);
1145
1444
  process.exit(1);
1146
1445
  }
1147
1446