minivibe 0.1.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/login.html ADDED
@@ -0,0 +1,331 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>MiniVibe CLI Login</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <style>
11
+ * { box-sizing: border-box; margin: 0; padding: 0; }
12
+
13
+ :root {
14
+ --primary: #667eea;
15
+ --primary-dark: #5a67d8;
16
+ --gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ --text: #1a202c;
18
+ --text-muted: #718096;
19
+ --bg: #f7fafc;
20
+ --border: #e2e8f0;
21
+ }
22
+
23
+ body {
24
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
25
+ background: var(--gradient);
26
+ min-height: 100vh;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ padding: 20px;
31
+ }
32
+
33
+ .container {
34
+ background: white;
35
+ padding: 48px 40px;
36
+ border-radius: 20px;
37
+ box-shadow: 0 25px 80px rgba(0, 0, 0, 0.3);
38
+ text-align: center;
39
+ max-width: 420px;
40
+ width: 100%;
41
+ }
42
+
43
+ .logo {
44
+ width: 64px;
45
+ height: 64px;
46
+ background: var(--gradient);
47
+ border-radius: 16px;
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+ margin: 0 auto 24px;
52
+ font-size: 32px;
53
+ color: white;
54
+ font-weight: 700;
55
+ }
56
+
57
+ h1 {
58
+ color: var(--text);
59
+ font-size: 28px;
60
+ font-weight: 700;
61
+ margin-bottom: 8px;
62
+ }
63
+
64
+ .subtitle {
65
+ color: var(--text-muted);
66
+ font-size: 15px;
67
+ margin-bottom: 36px;
68
+ }
69
+
70
+ .btn {
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ gap: 12px;
75
+ width: 100%;
76
+ padding: 14px 24px;
77
+ font-size: 15px;
78
+ font-weight: 500;
79
+ border: 1px solid var(--border);
80
+ border-radius: 10px;
81
+ background: white;
82
+ cursor: pointer;
83
+ transition: all 0.2s;
84
+ margin-bottom: 12px;
85
+ color: var(--text);
86
+ }
87
+
88
+ .btn:hover {
89
+ background: var(--bg);
90
+ border-color: #cbd5e0;
91
+ }
92
+
93
+ .btn:disabled {
94
+ opacity: 0.6;
95
+ cursor: not-allowed;
96
+ }
97
+
98
+ .btn img {
99
+ width: 20px;
100
+ height: 20px;
101
+ }
102
+
103
+ .status {
104
+ margin-top: 20px;
105
+ padding: 14px 16px;
106
+ border-radius: 10px;
107
+ font-size: 14px;
108
+ display: none;
109
+ }
110
+
111
+ .status.success {
112
+ display: block;
113
+ background: #c6f6d5;
114
+ color: #22543d;
115
+ }
116
+
117
+ .status.error {
118
+ display: block;
119
+ background: #fed7d7;
120
+ color: #c53030;
121
+ }
122
+
123
+ .status.loading {
124
+ display: block;
125
+ background: #e9d8fd;
126
+ color: #553c9a;
127
+ }
128
+
129
+ .close-msg {
130
+ margin-top: 20px;
131
+ color: var(--text-muted);
132
+ font-size: 14px;
133
+ }
134
+
135
+ .features {
136
+ margin-top: 32px;
137
+ padding-top: 24px;
138
+ border-top: 1px solid var(--border);
139
+ text-align: left;
140
+ }
141
+
142
+ .features h3 {
143
+ font-size: 13px;
144
+ font-weight: 600;
145
+ color: var(--text-muted);
146
+ text-transform: uppercase;
147
+ letter-spacing: 0.5px;
148
+ margin-bottom: 16px;
149
+ }
150
+
151
+ .feature {
152
+ display: flex;
153
+ align-items: flex-start;
154
+ gap: 12px;
155
+ margin-bottom: 14px;
156
+ }
157
+
158
+ .feature-icon {
159
+ font-size: 18px;
160
+ line-height: 1;
161
+ }
162
+
163
+ .feature-text {
164
+ font-size: 14px;
165
+ color: var(--text);
166
+ }
167
+
168
+ .success-content {
169
+ display: none;
170
+ }
171
+
172
+ .success-content.visible {
173
+ display: block;
174
+ }
175
+
176
+ .success-icon {
177
+ width: 80px;
178
+ height: 80px;
179
+ background: #c6f6d5;
180
+ border-radius: 50%;
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: center;
184
+ margin: 0 auto 24px;
185
+ font-size: 40px;
186
+ }
187
+
188
+ .next-steps {
189
+ text-align: left;
190
+ margin-top: 32px;
191
+ }
192
+
193
+ .next-step {
194
+ background: var(--bg);
195
+ padding: 16px;
196
+ border-radius: 10px;
197
+ margin-bottom: 12px;
198
+ }
199
+
200
+ .next-step-title {
201
+ font-weight: 600;
202
+ font-size: 14px;
203
+ margin-bottom: 8px;
204
+ color: var(--text);
205
+ }
206
+
207
+ .next-step-code {
208
+ background: #1a202c;
209
+ color: #e2e8f0;
210
+ padding: 10px 14px;
211
+ border-radius: 6px;
212
+ font-family: 'SF Mono', Monaco, monospace;
213
+ font-size: 13px;
214
+ }
215
+ </style>
216
+ </head>
217
+ <body>
218
+ <div class="container">
219
+ <!-- Login Content -->
220
+ <div id="loginContent">
221
+ <div class="logo">M</div>
222
+ <h1>MiniVibe</h1>
223
+ <p class="subtitle">Sign in to connect your CLI to your account</p>
224
+
225
+ <button id="signInBtn" class="btn" onclick="signIn()">
226
+ <img src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg" alt="Google">
227
+ Sign in with Google
228
+ </button>
229
+
230
+ <div id="status" class="status"></div>
231
+
232
+ <div class="features">
233
+ <h3>What you'll get</h3>
234
+ <div class="feature">
235
+ <span class="feature-icon">&#128241;</span>
236
+ <span class="feature-text">Control Claude Code sessions from your iPhone</span>
237
+ </div>
238
+ <div class="feature">
239
+ <span class="feature-icon">&#9889;</span>
240
+ <span class="feature-text">Approve permissions remotely</span>
241
+ </div>
242
+ <div class="feature">
243
+ <span class="feature-icon">&#128260;</span>
244
+ <span class="feature-text">Manage multiple sessions in parallel</span>
245
+ </div>
246
+ </div>
247
+ </div>
248
+
249
+ <!-- Success Content -->
250
+ <div id="successContent" class="success-content">
251
+ <div class="success-icon">&#10003;</div>
252
+ <h1>You're Connected!</h1>
253
+ <p class="subtitle" id="emailDisplay">Signed in as user@example.com</p>
254
+
255
+ <div class="next-steps">
256
+ <div class="next-step">
257
+ <div class="next-step-title">Start your first session</div>
258
+ <div class="next-step-code">vibe "Fix the login bug"</div>
259
+ </div>
260
+ <div class="next-step">
261
+ <div class="next-step-title">Or start with a name</div>
262
+ <div class="next-step-code">vibe --name "Auth Fix" "Fix bug"</div>
263
+ </div>
264
+ </div>
265
+
266
+ <p class="close-msg">You can close this window and return to your terminal.</p>
267
+ </div>
268
+ </div>
269
+
270
+ <script type="module">
271
+ import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js';
272
+ import { getAuth, signInWithPopup, GoogleAuthProvider } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js';
273
+
274
+ // Firebase config will be injected by the server
275
+ const firebaseConfig = window.FIREBASE_CONFIG;
276
+
277
+ if (!firebaseConfig) {
278
+ showStatus('error', 'Firebase configuration not found. Check your setup.');
279
+ } else {
280
+ const app = initializeApp(firebaseConfig);
281
+ const auth = getAuth(app);
282
+ const provider = new GoogleAuthProvider();
283
+
284
+ window.signIn = async function() {
285
+ const btn = document.getElementById('signInBtn');
286
+ btn.disabled = true;
287
+ showStatus('loading', 'Opening sign-in popup...');
288
+
289
+ try {
290
+ const result = await signInWithPopup(auth, provider);
291
+ const idToken = await result.user.getIdToken();
292
+ // Get refresh token for auto-refresh capability
293
+ const refreshToken = result.user.stsTokenManager?.refreshToken || null;
294
+
295
+ showStatus('loading', 'Saving credentials...');
296
+
297
+ // Send token to local server
298
+ const response = await fetch('/callback', {
299
+ method: 'POST',
300
+ headers: { 'Content-Type': 'application/json' },
301
+ body: JSON.stringify({
302
+ token: idToken,
303
+ refreshToken: refreshToken,
304
+ email: result.user.email
305
+ })
306
+ });
307
+
308
+ if (response.ok) {
309
+ // Show success screen
310
+ document.getElementById('loginContent').style.display = 'none';
311
+ document.getElementById('successContent').classList.add('visible');
312
+ document.getElementById('emailDisplay').textContent = `Signed in as ${result.user.email}`;
313
+ } else {
314
+ throw new Error('Failed to save token');
315
+ }
316
+ } catch (error) {
317
+ console.error(error);
318
+ showStatus('error', error.message || 'Sign in failed. Please try again.');
319
+ btn.disabled = false;
320
+ }
321
+ };
322
+ }
323
+
324
+ function showStatus(type, message) {
325
+ const status = document.getElementById('status');
326
+ status.className = 'status ' + type;
327
+ status.textContent = message;
328
+ }
329
+ </script>
330
+ </body>
331
+ </html>
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "minivibe",
3
+ "version": "0.1.0",
4
+ "description": "CLI wrapper for Claude Code with mobile remote control",
5
+ "author": "neng.ai",
6
+ "homepage": "https://github.com/python3isfun/neng",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/python3isfun/neng.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/python3isfun/neng/issues"
13
+ },
14
+ "main": "vibe.js",
15
+ "bin": {
16
+ "vibe": "./vibe.js",
17
+ "vibe-agent": "./agent/agent.js"
18
+ },
19
+ "files": [
20
+ "vibe.js",
21
+ "login.html",
22
+ "pty-wrapper.py",
23
+ "pty-wrapper-node.js",
24
+ "agent/"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ },
29
+ "scripts": {
30
+ "start": "node vibe.js"
31
+ },
32
+ "dependencies": {
33
+ "uuid": "^9.0.0",
34
+ "ws": "^8.14.2"
35
+ },
36
+ "optionalDependencies": {
37
+ "node-pty": "^1.0.0"
38
+ },
39
+ "keywords": [
40
+ "claude",
41
+ "claude-code",
42
+ "cli",
43
+ "remote",
44
+ "mobile",
45
+ "ai",
46
+ "terminal"
47
+ ],
48
+ "license": "MIT"
49
+ }
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Node.js PTY wrapper for Claude Code (Windows compatible)
4
+ *
5
+ * Alternative to pty-wrapper.py for Windows users.
6
+ * Uses node-pty for cross-platform pseudo-terminal support.
7
+ *
8
+ * Usage: node pty-wrapper-node.js <command> [args...]
9
+ */
10
+
11
+ const os = require('os');
12
+ const path = require('path');
13
+
14
+ // Try to load node-pty (optional dependency)
15
+ let pty;
16
+ try {
17
+ pty = require('node-pty');
18
+ } catch (err) {
19
+ console.error('Error: node-pty is not installed.');
20
+ console.error('Install it with: npm install node-pty');
21
+ console.error('');
22
+ console.error('On Windows, you may also need:');
23
+ console.error(' - Python 3.x');
24
+ console.error(' - Visual Studio Build Tools');
25
+ console.error(' npm install --global windows-build-tools');
26
+ process.exit(1);
27
+ }
28
+
29
+ // Get command to run
30
+ const args = process.argv.slice(2);
31
+ if (args.length === 0) {
32
+ console.error('Usage: pty-wrapper-node.js <command> [args...]');
33
+ process.exit(1);
34
+ }
35
+
36
+ const command = args[0];
37
+ const commandArgs = args.slice(1);
38
+
39
+ // Get terminal size
40
+ const cols = process.stdout.columns || 80;
41
+ const rows = process.stdout.rows || 24;
42
+
43
+ // Buffer for detecting permission prompts
44
+ let outputBuffer = '';
45
+ const MAX_BUFFER = 2048;
46
+
47
+ // ANSI escape code pattern
48
+ const ansiPattern = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b[PX^_].*?\x1b\\/g;
49
+
50
+ function stripAnsi(text) {
51
+ return text.replace(ansiPattern, '');
52
+ }
53
+
54
+ function detectPermissionPrompt(text) {
55
+ const clean = stripAnsi(text);
56
+ const lines = clean.split('\n');
57
+
58
+ const options = [];
59
+ let question = null;
60
+
61
+ for (const line of lines) {
62
+ const trimmed = line.trim();
63
+
64
+ // Detect question line
65
+ if (/want to|allow|proceed/i.test(trimmed) && trimmed.includes('?')) {
66
+ question = trimmed;
67
+ }
68
+
69
+ // Detect numbered options (1. Yes, 2. Yes and don't ask, 3. Type here)
70
+ const match = trimmed.match(/^[›\s]*(\d+)\.\s+(.+)$/);
71
+ if (match) {
72
+ options.push({
73
+ id: parseInt(match[1]),
74
+ label: match[2].trim(),
75
+ requiresInput: /type|tell/i.test(match[2])
76
+ });
77
+ }
78
+ }
79
+
80
+ // Only return if we found valid options
81
+ if (options.length >= 2) {
82
+ return {
83
+ type: 'permission_prompt',
84
+ question: question || 'Permission required',
85
+ options
86
+ };
87
+ }
88
+
89
+ return null;
90
+ }
91
+
92
+ // Spawn the PTY process
93
+ const ptyProcess = pty.spawn(command, commandArgs, {
94
+ name: 'xterm-256color',
95
+ cols,
96
+ rows,
97
+ cwd: process.cwd(),
98
+ env: process.env
99
+ });
100
+
101
+ // Handle terminal resize
102
+ process.stdout.on('resize', () => {
103
+ ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
104
+ });
105
+
106
+ // Forward PTY output to stdout
107
+ ptyProcess.onData((data) => {
108
+ process.stdout.write(data);
109
+
110
+ // Buffer output for prompt detection
111
+ outputBuffer += data;
112
+ if (outputBuffer.length > MAX_BUFFER) {
113
+ outputBuffer = outputBuffer.slice(-MAX_BUFFER);
114
+ }
115
+
116
+ // Try to detect permission prompt
117
+ const prompt = detectPermissionPrompt(outputBuffer);
118
+ if (prompt) {
119
+ // Write to fd 3 if available (for vibe-cli to read)
120
+ try {
121
+ const fs = require('fs');
122
+ const jsonLine = JSON.stringify(prompt) + '\n';
123
+ fs.writeSync(3, jsonLine);
124
+ } catch (err) {
125
+ // FD 3 not available, skip
126
+ }
127
+ outputBuffer = '';
128
+ }
129
+ });
130
+
131
+ // Forward stdin to PTY
132
+ if (process.stdin.isTTY) {
133
+ process.stdin.setRawMode(true);
134
+ }
135
+ process.stdin.resume();
136
+ process.stdin.on('data', (data) => {
137
+ ptyProcess.write(data.toString());
138
+ });
139
+
140
+ // Handle PTY exit
141
+ ptyProcess.onExit(({ exitCode, signal }) => {
142
+ if (process.stdin.isTTY) {
143
+ process.stdin.setRawMode(false);
144
+ }
145
+ process.exit(exitCode);
146
+ });
147
+
148
+ // Handle signals (Unix only - Windows doesn't have these)
149
+ if (os.platform() !== 'win32') {
150
+ process.on('SIGINT', () => {
151
+ ptyProcess.kill('SIGINT');
152
+ });
153
+
154
+ process.on('SIGTERM', () => {
155
+ ptyProcess.kill('SIGTERM');
156
+ });
157
+ }