minivibe 0.1.0 → 0.1.1

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/README.md +180 -0
  2. package/package.json +1 -1
  3. package/vibe.js +118 -22
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # vibe-cli
2
+
3
+ CLI wrapper for Claude Code with mobile remote control via MiniVibe.
4
+
5
+ ## Features
6
+
7
+ - Remote control Claude Code from your iOS device
8
+ - Session management with resume capability
9
+ - Permission handling from mobile
10
+ - Token usage tracking
11
+ - Headless authentication for servers (EC2, etc.)
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # Clone the repository
17
+ git clone <repo-url>
18
+ cd vibe-cli
19
+
20
+ # Install dependencies
21
+ npm install
22
+
23
+ # Link globally (optional)
24
+ npm link
25
+ ```
26
+
27
+ ### Global Installation (EC2/Servers)
28
+
29
+ ```bash
30
+ # Install to /opt for all users
31
+ sudo git clone <repo-url> /opt/neng
32
+ cd /opt/neng/vibe-cli
33
+ sudo npm install
34
+ sudo npm link
35
+
36
+ # Or add to PATH
37
+ echo 'export PATH="/opt/neng/vibe-cli:$PATH"' | sudo tee /etc/profile.d/vibe.sh
38
+ ```
39
+
40
+ ## Authentication
41
+
42
+ ### Browser Login (Desktop)
43
+
44
+ ```bash
45
+ vibe --login
46
+ ```
47
+
48
+ Opens browser for Google sign-in. Token is saved to `~/.vibe/auth.json`.
49
+
50
+ ### Headless Login (Servers/EC2)
51
+
52
+ ```bash
53
+ vibe --login --headless
54
+ ```
55
+
56
+ Displays a device code. Visit the URL on any device to authenticate.
57
+
58
+ ### Manual Token
59
+
60
+ ```bash
61
+ vibe --token <firebase-token>
62
+ ```
63
+
64
+ Get token from MiniVibe iOS app Settings > Copy Token for CLI.
65
+
66
+ ## Usage
67
+
68
+ ### Basic Usage
69
+
70
+ ```bash
71
+ # Interactive session
72
+ vibe
73
+
74
+ # With initial prompt
75
+ vibe "Fix the bug in main.js"
76
+
77
+ # Connect to bridge server
78
+ vibe --bridge wss://ws.neng.ai
79
+
80
+ # With prompt and bridge
81
+ vibe --bridge wss://ws.neng.ai "Deploy the application"
82
+ ```
83
+
84
+ ### Session Management
85
+
86
+ ```bash
87
+ # Name your session (visible in iOS app)
88
+ vibe --name "Backend Refactor" --bridge wss://ws.neng.ai
89
+
90
+ # Resume a previous session
91
+ vibe --resume <session-id> --bridge wss://ws.neng.ai
92
+ ```
93
+
94
+ ### Skip Permissions Mode
95
+
96
+ For automated/headless environments where you trust the execution context:
97
+
98
+ ```bash
99
+ vibe --dangerously-skip-permissions --bridge wss://ws.neng.ai
100
+ ```
101
+
102
+ **Warning:** This mode auto-approves ALL tool executions (commands, file writes, etc.) without prompting. Only use in trusted/sandboxed environments.
103
+
104
+ ### Local Agent Mode
105
+
106
+ Connect via a local vibe-agent for managed sessions:
107
+
108
+ ```bash
109
+ # Auto-discover agent
110
+ vibe --agent
111
+
112
+ # Specify agent URL
113
+ vibe --agent ws://localhost:9999
114
+ ```
115
+
116
+ ## Options
117
+
118
+ | Option | Description |
119
+ |--------|-------------|
120
+ | `--bridge <url>` | Connect to bridge server (e.g., `wss://ws.neng.ai`) |
121
+ | `--agent [url]` | Connect via local vibe-agent |
122
+ | `--name <name>` | Name this session (shown in mobile app) |
123
+ | `--resume <id>` | Resume a previous session |
124
+ | `--login` | Sign in with Google (browser) |
125
+ | `--headless` | Use device code flow for headless environments |
126
+ | `--token <token>` | Set Firebase auth token manually |
127
+ | `--logout` | Remove stored auth token |
128
+ | `--dangerously-skip-permissions` | Auto-approve all tool executions |
129
+ | `--node-pty` | Use Node.js PTY wrapper (required for Windows) |
130
+ | `--help, -h` | Show help message |
131
+
132
+ ## In-Session Commands
133
+
134
+ | Command | Description |
135
+ |---------|-------------|
136
+ | `/name <name>` | Rename the current session |
137
+
138
+ ## Requirements
139
+
140
+ - Node.js >= 18.0.0
141
+ - Claude Code CLI installed and in PATH
142
+ - Python 3 (for Unix PTY wrapper) or node-pty (for Windows)
143
+
144
+ ## Platform Notes
145
+
146
+ ### macOS/Linux
147
+
148
+ Uses Python PTY wrapper by default. Requires `python3`.
149
+
150
+ ### Windows
151
+
152
+ Requires `node-pty`:
153
+
154
+ ```bash
155
+ npm install node-pty
156
+ ```
157
+
158
+ May also need Visual Studio Build Tools and Python for native compilation.
159
+
160
+ ## Files
161
+
162
+ - `~/.vibe/auth.json` - Stored authentication (token + refresh token)
163
+ - `~/.vibe/token` - Legacy token file (backwards compatibility)
164
+
165
+ ## Architecture
166
+
167
+ ```
168
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
169
+ │ MiniVibe │────▶│ Bridge │◀────│ vibe-cli │
170
+ │ iOS App │ │ Server │ │ + Claude │
171
+ └─────────────┘ └─────────────┘ └─────────────┘
172
+ ```
173
+
174
+ 1. **vibe-cli** wraps Claude Code and connects to the bridge server
175
+ 2. **Bridge server** relays messages between mobile and CLI
176
+ 3. **MiniVibe iOS** provides remote control, permission approval, and session monitoring
177
+
178
+ ## License
179
+
180
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minivibe",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "CLI wrapper for Claude Code with mobile remote control",
5
5
  "author": "neng.ai",
6
6
  "homepage": "https://github.com/python3isfun/neng",
package/vibe.js CHANGED
@@ -382,6 +382,7 @@ let authToken = null;
382
382
  let headlessMode = false;
383
383
  let sessionName = null;
384
384
  let useNodePty = os.platform() === 'win32'; // Auto-detect Windows, can be overridden
385
+ let skipPermissions = false; // --dangerously-skip-permissions mode
385
386
 
386
387
  for (let i = 0; i < args.length; i++) {
387
388
  if (args[i] === '--help' || args[i] === '-h') {
@@ -407,6 +408,7 @@ Options:
407
408
  --token <token> Set Firebase auth token manually
408
409
  --logout Remove stored auth token
409
410
  --node-pty Use Node.js PTY wrapper (required for Windows, optional for Unix)
411
+ --dangerously-skip-permissions Run Claude without permission prompts (use with caution!)
410
412
  --help, -h Show this help message
411
413
 
412
414
  In-Session Commands:
@@ -466,6 +468,8 @@ Examples:
466
468
  process.exit(0);
467
469
  } else if (args[i] === '--node-pty') {
468
470
  useNodePty = true;
471
+ } else if (args[i] === '--dangerously-skip-permissions') {
472
+ skipPermissions = true;
469
473
  } else if (!args[i].startsWith('--')) {
470
474
  initialPrompt = args[i];
471
475
  }
@@ -534,12 +538,50 @@ function logStatus(msg) {
534
538
 
535
539
  // Get Claude session file path
536
540
  // Claude uses path with '/' replaced by '-' (not base64)
541
+ // But the exact format may vary - we try multiple strategies
537
542
  function getSessionFilePath() {
538
- const projectPathHash = process.cwd().replace(/\//g, '-');
539
- const claudeDir = path.join(os.homedir(), '.claude', 'projects', projectPathHash);
540
- return path.join(claudeDir, `${sessionId}.jsonl`);
543
+ const cwd = process.cwd();
544
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects');
545
+
546
+ // Strategy 1: Direct replacement (e.g., /home/ubuntu -> -home-ubuntu)
547
+ const directHash = cwd.replace(/\//g, '-');
548
+
549
+ // Strategy 2: Without leading dash (e.g., home-ubuntu)
550
+ const noLeadingDash = directHash.replace(/^-/, '');
551
+
552
+ // Strategy 3: URL-safe encoding variations
553
+ const candidates = [directHash, noLeadingDash];
554
+
555
+ // Try to find existing directory that matches
556
+ if (fs.existsSync(projectsDir)) {
557
+ for (const candidate of candidates) {
558
+ const candidateDir = path.join(projectsDir, candidate);
559
+ if (fs.existsSync(candidateDir)) {
560
+ return path.join(candidateDir, `${sessionId}.jsonl`);
561
+ }
562
+ }
563
+
564
+ // Scan for session file in any project directory
565
+ try {
566
+ const dirs = fs.readdirSync(projectsDir);
567
+ for (const dir of dirs) {
568
+ const sessionFile = path.join(projectsDir, dir, `${sessionId}.jsonl`);
569
+ if (fs.existsSync(sessionFile)) {
570
+ return sessionFile;
571
+ }
572
+ }
573
+ } catch (err) {
574
+ // Ignore scan errors
575
+ }
576
+ }
577
+
578
+ // Fall back to direct hash (will be created by Claude)
579
+ return path.join(projectsDir, directHash, `${sessionId}.jsonl`);
541
580
  }
542
581
 
582
+ // Cache for discovered session file path
583
+ let discoveredSessionFile = null;
584
+
543
585
  // Get local IP
544
586
  function getLocalIP() {
545
587
  const interfaces = os.networkInterfaces();
@@ -910,28 +952,41 @@ function sendToBridge(data) {
910
952
  // ====================
911
953
 
912
954
  function startSessionFileWatcher() {
913
- const sessionFile = getSessionFilePath();
914
- logStatus(`Watching session file: ${sessionFile}`);
955
+ let sessionFile = getSessionFilePath();
956
+ logStatus(`Watching for session file: ${sessionFile}`);
915
957
 
916
- // Also check what files actually exist in the projects directory
917
958
  const projectsDir = path.join(os.homedir(), '.claude', 'projects');
918
- if (fs.existsSync(projectsDir)) {
919
- const dirs = fs.readdirSync(projectsDir);
920
- logStatus(`Found ${dirs.length} project directories in ${projectsDir}`);
921
- // Look for our session in any directory
922
- for (const dir of dirs.slice(0, 5)) { // Check first 5
923
- const fullDir = path.join(projectsDir, dir);
924
- if (fs.statSync(fullDir).isDirectory()) {
925
- const files = fs.readdirSync(fullDir);
926
- const sessionFiles = files.filter(f => f.includes(sessionId.slice(0, 8)));
927
- if (sessionFiles.length > 0) {
928
- logStatus(`Found session files in ${dir}: ${sessionFiles.join(', ')}`);
959
+ let fileCheckCount = 0;
960
+ let lastNotFoundLog = 0;
961
+ let fileFound = false;
962
+
963
+ // Function to scan for session file in all project directories
964
+ function scanForSessionFile() {
965
+ if (!fs.existsSync(projectsDir)) return null;
966
+
967
+ try {
968
+ const dirs = fs.readdirSync(projectsDir);
969
+ // Limit scan to 50 directories to avoid slowdown
970
+ const dirsToScan = dirs.slice(0, 50);
971
+ for (const dir of dirsToScan) {
972
+ const fullDir = path.join(projectsDir, dir);
973
+ try {
974
+ if (fs.statSync(fullDir).isDirectory()) {
975
+ const candidateFile = path.join(fullDir, `${sessionId}.jsonl`);
976
+ if (fs.existsSync(candidateFile)) {
977
+ return candidateFile;
978
+ }
979
+ }
980
+ } catch (e) {
981
+ // Skip inaccessible directories
929
982
  }
930
983
  }
984
+ } catch (err) {
985
+ // Ignore scan errors
931
986
  }
987
+ return null;
932
988
  }
933
989
 
934
- let fileCheckCount = 0;
935
990
  const pollInterval = setInterval(() => {
936
991
  if (!isRunning || isShuttingDown) {
937
992
  clearInterval(pollInterval);
@@ -939,15 +994,46 @@ function startSessionFileWatcher() {
939
994
  }
940
995
 
941
996
  try {
942
- if (!fs.existsSync(sessionFile)) {
943
- // Log occasionally that file doesn't exist yet
944
- if (fileCheckCount++ % 20 === 0) {
945
- logStatus(`Session file not found yet: ${sessionFile}`);
997
+ // If file not found yet, try to discover it
998
+ if (!fileFound) {
999
+ if (!fs.existsSync(sessionFile)) {
1000
+ // Try scanning for it every 5 seconds
1001
+ fileCheckCount++;
1002
+ if (fileCheckCount % 10 === 0) {
1003
+ const discovered = scanForSessionFile();
1004
+ if (discovered) {
1005
+ sessionFile = discovered;
1006
+ fileFound = true;
1007
+ lastFileSize = 0; // Reset to read from beginning
1008
+ log(`📁 Found session file: ${sessionFile}`, colors.dim);
1009
+ } else {
1010
+ // Only log "not found" every 30 seconds to reduce spam
1011
+ const now = Date.now();
1012
+ if (now - lastNotFoundLog > 30000) {
1013
+ logStatus(`Waiting for session file... (${Math.floor(fileCheckCount / 2)}s)`);
1014
+ lastNotFoundLog = now;
1015
+ }
1016
+ }
1017
+ }
1018
+ return;
946
1019
  }
1020
+ fileFound = true;
1021
+ lastFileSize = 0; // Reset to read from beginning
1022
+ log(`📁 Session file ready: ${sessionFile}`, colors.dim);
1023
+ }
1024
+
1025
+ if (!fs.existsSync(sessionFile)) {
947
1026
  return;
948
1027
  }
949
1028
 
950
1029
  const stats = fs.statSync(sessionFile);
1030
+
1031
+ // Handle file being recreated (size shrunk)
1032
+ if (stats.size < lastFileSize) {
1033
+ logStatus(`Session file was recreated, re-reading from start`);
1034
+ lastFileSize = 0;
1035
+ }
1036
+
951
1037
  if (stats.size > lastFileSize) {
952
1038
  logStatus(`Session file changed: ${lastFileSize} -> ${stats.size} bytes`);
953
1039
  const fd = fs.openSync(sessionFile, 'r');
@@ -1277,6 +1363,12 @@ function startClaude() {
1277
1363
  logStatus(`Starting new Claude session: ${sessionId.slice(0, 8)}...`);
1278
1364
  }
1279
1365
 
1366
+ // Add --dangerously-skip-permissions if requested
1367
+ if (skipPermissions) {
1368
+ claudeArgs.push('--dangerously-skip-permissions');
1369
+ log('⚠️ Running in skip-permissions mode (no prompts)', colors.yellow);
1370
+ }
1371
+
1280
1372
  // Choose PTY wrapper based on platform/flag
1281
1373
  // - Windows: always use node-pty (Python PTY doesn't work)
1282
1374
  // - Unix: use Python by default, node-pty with --node-pty flag
@@ -1593,6 +1685,10 @@ function main() {
1593
1685
  log(` Mode: Local (no bridge - terminal only)`, colors.dim);
1594
1686
  }
1595
1687
 
1688
+ if (skipPermissions) {
1689
+ log(` Perms: SKIPPED (dangerously-skip-permissions)`, colors.yellow);
1690
+ }
1691
+
1596
1692
  log(` Terminal: ${process.cwd()}`, colors.dim);
1597
1693
  log('══════════════════════════════════════', colors.dim);
1598
1694
  console.log('');