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/agent/agent.js +1218 -0
- package/login.html +331 -0
- package/package.json +49 -0
- package/pty-wrapper-node.js +157 -0
- package/pty-wrapper.py +225 -0
- package/vibe.js +1621 -0
package/pty-wrapper.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PTY wrapper for Claude Code.
|
|
4
|
+
Creates a pseudo-terminal and runs Claude, allowing both terminal and piped input.
|
|
5
|
+
Also captures permission prompts and outputs them as JSON to a separate FD.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import pty
|
|
10
|
+
import select
|
|
11
|
+
import signal
|
|
12
|
+
import termios
|
|
13
|
+
import tty
|
|
14
|
+
import struct
|
|
15
|
+
import fcntl
|
|
16
|
+
import re
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
# Buffer for detecting permission prompts
|
|
20
|
+
output_buffer = ""
|
|
21
|
+
PROMPT_FD = 3 # File descriptor for permission prompt output
|
|
22
|
+
|
|
23
|
+
def set_winsize(fd, rows, cols):
|
|
24
|
+
"""Set terminal window size."""
|
|
25
|
+
winsize = struct.pack('HHHH', rows, cols, 0, 0)
|
|
26
|
+
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
|
|
27
|
+
|
|
28
|
+
def strip_ansi(text):
|
|
29
|
+
"""Remove ANSI escape codes from text."""
|
|
30
|
+
ansi_pattern = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b[PX^_].*?\x1b\\')
|
|
31
|
+
return ansi_pattern.sub('', text)
|
|
32
|
+
|
|
33
|
+
def detect_permission_prompt(text):
|
|
34
|
+
"""
|
|
35
|
+
Detect Claude's permission prompt in terminal output.
|
|
36
|
+
Returns parsed prompt data or None.
|
|
37
|
+
"""
|
|
38
|
+
clean = strip_ansi(text)
|
|
39
|
+
|
|
40
|
+
# Look for the permission prompt pattern
|
|
41
|
+
# Claude shows: "Do you want to proceed?" or similar, followed by numbered options
|
|
42
|
+
lines = clean.split('\n')
|
|
43
|
+
|
|
44
|
+
options = []
|
|
45
|
+
question = None
|
|
46
|
+
|
|
47
|
+
for i, line in enumerate(lines):
|
|
48
|
+
line = line.strip()
|
|
49
|
+
|
|
50
|
+
# Detect question line
|
|
51
|
+
if 'want to' in line.lower() or 'allow' in line.lower() or 'proceed' in line.lower():
|
|
52
|
+
if '?' in line:
|
|
53
|
+
question = line
|
|
54
|
+
|
|
55
|
+
# Detect numbered options (1. Yes, 2. Yes and don't ask, 3. Type here)
|
|
56
|
+
match = re.match(r'^[›\s]*(\d+)\.\s+(.+)$', line)
|
|
57
|
+
if match:
|
|
58
|
+
opt_id = int(match.group(1))
|
|
59
|
+
opt_label = match.group(2).strip()
|
|
60
|
+
options.append({
|
|
61
|
+
'id': opt_id,
|
|
62
|
+
'label': opt_label,
|
|
63
|
+
'requiresInput': 'type' in opt_label.lower() or 'tell' in opt_label.lower()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
# Only return if we found valid options
|
|
67
|
+
if len(options) >= 2:
|
|
68
|
+
return {
|
|
69
|
+
'type': 'permission_prompt',
|
|
70
|
+
'question': question or 'Permission required',
|
|
71
|
+
'options': options
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def send_prompt_to_fd(prompt_data):
|
|
77
|
+
"""Send detected prompt to FD 3 if available."""
|
|
78
|
+
try:
|
|
79
|
+
# Check if FD 3 is open
|
|
80
|
+
os.fstat(PROMPT_FD)
|
|
81
|
+
json_line = json.dumps(prompt_data) + '\n'
|
|
82
|
+
os.write(PROMPT_FD, json_line.encode('utf-8'))
|
|
83
|
+
# Debug: also write to stderr so we can see it
|
|
84
|
+
sys.stderr.write(f"[PTY] Detected prompt: {prompt_data.get('question', 'unknown')}, options: {len(prompt_data.get('options', []))}\n")
|
|
85
|
+
sys.stderr.flush()
|
|
86
|
+
except OSError:
|
|
87
|
+
# FD 3 not available, skip
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def main():
|
|
91
|
+
# Get command to run
|
|
92
|
+
if len(sys.argv) < 2:
|
|
93
|
+
print("Usage: pty-wrapper.py <command> [args...]", file=sys.stderr)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
cmd = sys.argv[1:]
|
|
97
|
+
|
|
98
|
+
# Save original terminal settings if stdin is a tty
|
|
99
|
+
stdin_is_tty = os.isatty(sys.stdin.fileno())
|
|
100
|
+
if stdin_is_tty:
|
|
101
|
+
old_settings = termios.tcgetattr(sys.stdin)
|
|
102
|
+
|
|
103
|
+
# Create pseudo-terminal
|
|
104
|
+
master_fd, slave_fd = pty.openpty()
|
|
105
|
+
|
|
106
|
+
# Get terminal size and apply to PTY
|
|
107
|
+
if stdin_is_tty:
|
|
108
|
+
try:
|
|
109
|
+
rows, cols = os.get_terminal_size()
|
|
110
|
+
set_winsize(slave_fd, rows, cols)
|
|
111
|
+
except OSError:
|
|
112
|
+
set_winsize(slave_fd, 24, 80)
|
|
113
|
+
else:
|
|
114
|
+
set_winsize(slave_fd, 24, 80)
|
|
115
|
+
|
|
116
|
+
# Fork process
|
|
117
|
+
pid = os.fork()
|
|
118
|
+
|
|
119
|
+
if pid == 0:
|
|
120
|
+
# Child process
|
|
121
|
+
os.close(master_fd)
|
|
122
|
+
|
|
123
|
+
# Create new session and set controlling terminal
|
|
124
|
+
os.setsid()
|
|
125
|
+
fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
|
|
126
|
+
|
|
127
|
+
# Redirect stdio to slave PTY
|
|
128
|
+
os.dup2(slave_fd, 0)
|
|
129
|
+
os.dup2(slave_fd, 1)
|
|
130
|
+
os.dup2(slave_fd, 2)
|
|
131
|
+
|
|
132
|
+
if slave_fd > 2:
|
|
133
|
+
os.close(slave_fd)
|
|
134
|
+
|
|
135
|
+
# Execute command
|
|
136
|
+
os.execvp(cmd[0], cmd)
|
|
137
|
+
|
|
138
|
+
# Parent process
|
|
139
|
+
os.close(slave_fd)
|
|
140
|
+
|
|
141
|
+
# Set stdin to raw mode if it's a tty
|
|
142
|
+
if stdin_is_tty:
|
|
143
|
+
tty.setraw(sys.stdin.fileno())
|
|
144
|
+
|
|
145
|
+
# Handle window resize
|
|
146
|
+
def handle_sigwinch(signum, frame):
|
|
147
|
+
if stdin_is_tty:
|
|
148
|
+
try:
|
|
149
|
+
rows, cols = os.get_terminal_size()
|
|
150
|
+
set_winsize(master_fd, rows, cols)
|
|
151
|
+
except OSError:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
signal.signal(signal.SIGWINCH, handle_sigwinch)
|
|
155
|
+
|
|
156
|
+
exit_code = 0
|
|
157
|
+
try:
|
|
158
|
+
while True:
|
|
159
|
+
# Wait for data from stdin or master PTY
|
|
160
|
+
rlist = [sys.stdin.fileno(), master_fd]
|
|
161
|
+
try:
|
|
162
|
+
readable, _, _ = select.select(rlist, [], [], 0.1)
|
|
163
|
+
except select.error:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
if sys.stdin.fileno() in readable:
|
|
167
|
+
# Data from stdin -> send to PTY
|
|
168
|
+
try:
|
|
169
|
+
data = os.read(sys.stdin.fileno(), 1024)
|
|
170
|
+
if not data:
|
|
171
|
+
break
|
|
172
|
+
os.write(master_fd, data)
|
|
173
|
+
except OSError:
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
if master_fd in readable:
|
|
177
|
+
# Data from PTY -> send to stdout and check for permission prompts
|
|
178
|
+
global output_buffer
|
|
179
|
+
try:
|
|
180
|
+
data = os.read(master_fd, 1024)
|
|
181
|
+
if not data:
|
|
182
|
+
break
|
|
183
|
+
os.write(sys.stdout.fileno(), data)
|
|
184
|
+
sys.stdout.flush()
|
|
185
|
+
|
|
186
|
+
# Buffer output for prompt detection
|
|
187
|
+
try:
|
|
188
|
+
text = data.decode('utf-8', errors='replace')
|
|
189
|
+
output_buffer += text
|
|
190
|
+
# Keep buffer reasonable size (last 2KB)
|
|
191
|
+
if len(output_buffer) > 2048:
|
|
192
|
+
output_buffer = output_buffer[-2048:]
|
|
193
|
+
|
|
194
|
+
# Try to detect permission prompt
|
|
195
|
+
prompt = detect_permission_prompt(output_buffer)
|
|
196
|
+
if prompt:
|
|
197
|
+
send_prompt_to_fd(prompt)
|
|
198
|
+
# Clear buffer after sending prompt
|
|
199
|
+
output_buffer = ""
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
except OSError:
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
# Check if child has exited
|
|
206
|
+
result = os.waitpid(pid, os.WNOHANG)
|
|
207
|
+
if result[0] != 0:
|
|
208
|
+
exit_code = os.WEXITSTATUS(result[1]) if os.WIFEXITED(result[1]) else 1
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
except KeyboardInterrupt:
|
|
212
|
+
# Send SIGINT to child
|
|
213
|
+
os.kill(pid, signal.SIGINT)
|
|
214
|
+
os.waitpid(pid, 0)
|
|
215
|
+
|
|
216
|
+
finally:
|
|
217
|
+
# Restore terminal settings
|
|
218
|
+
if stdin_is_tty:
|
|
219
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|
|
220
|
+
os.close(master_fd)
|
|
221
|
+
|
|
222
|
+
sys.exit(exit_code)
|
|
223
|
+
|
|
224
|
+
if __name__ == '__main__':
|
|
225
|
+
main()
|