aigo 1.0.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/AGENTS.md +1 -0
- package/README.md +453 -0
- package/bin/vigo.js +282 -0
- package/lib/cli.js +81 -0
- package/lib/config.js +80 -0
- package/lib/cursor.js +74 -0
- package/lib/port.js +39 -0
- package/lib/server.js +284 -0
- package/lib/tmux.js +86 -0
- package/lib/tunnel.js +150 -0
- package/package.json +27 -0
- package/public/index.html +545 -0
- package/public/terminal.js +759 -0
- package/specs/claude-code-web-terminal.md +258 -0
- package/specs/cursor-cli-support.md +139 -0
package/lib/server.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { WebSocketServer } from 'ws';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import pty from 'node-pty';
|
|
7
|
+
import { getTmuxPath, killSession } from './tmux.js';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Start the web server with WebSocket support
|
|
14
|
+
*/
|
|
15
|
+
export function startServer({ port, token, password, session, lockTimeout, exitTimeout }) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const app = express();
|
|
18
|
+
const server = createServer(app);
|
|
19
|
+
|
|
20
|
+
// Track last activity for exit timeout
|
|
21
|
+
let lastActivityTime = Date.now();
|
|
22
|
+
let exitTimer = null;
|
|
23
|
+
let hasActiveConnection = false;
|
|
24
|
+
|
|
25
|
+
// Update activity timestamp
|
|
26
|
+
const updateActivity = () => {
|
|
27
|
+
lastActivityTime = Date.now();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Start exit timeout checker
|
|
31
|
+
const startExitTimer = () => {
|
|
32
|
+
if (exitTimeout <= 0 || exitTimer) return;
|
|
33
|
+
|
|
34
|
+
exitTimer = setInterval(() => {
|
|
35
|
+
const inactiveMinutes = (Date.now() - lastActivityTime) / 1000 / 60;
|
|
36
|
+
|
|
37
|
+
if (inactiveMinutes >= exitTimeout) {
|
|
38
|
+
console.log(`\nSession inactive for ${exitTimeout} minutes. Exiting...`);
|
|
39
|
+
|
|
40
|
+
// Kill tmux session
|
|
41
|
+
killSession(session);
|
|
42
|
+
console.log(`Killed tmux session: ${session}`);
|
|
43
|
+
|
|
44
|
+
// Notify all connected clients
|
|
45
|
+
wss.clients.forEach(client => {
|
|
46
|
+
if (client.readyState === client.OPEN) {
|
|
47
|
+
client.send(JSON.stringify({ type: 'session_expired' }));
|
|
48
|
+
client.close(1000, 'Session expired due to inactivity');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Stop timer and exit
|
|
53
|
+
clearInterval(exitTimer);
|
|
54
|
+
server.close();
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
}, 60000); // Check every minute
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Serve static files from public directory
|
|
61
|
+
const publicPath = path.join(__dirname, '..', 'public');
|
|
62
|
+
app.use('/static', express.static(publicPath));
|
|
63
|
+
|
|
64
|
+
// Token validation middleware
|
|
65
|
+
const validateToken = (req, res, next) => {
|
|
66
|
+
const urlToken = req.params.token;
|
|
67
|
+
if (urlToken !== token) {
|
|
68
|
+
return res.status(403).send('Forbidden: Invalid token');
|
|
69
|
+
}
|
|
70
|
+
next();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Serve terminal page at /t/:token
|
|
74
|
+
app.get('/t/:token', validateToken, (req, res) => {
|
|
75
|
+
res.sendFile(path.join(publicPath, 'index.html'));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Health check endpoint
|
|
79
|
+
app.get('/health', (req, res) => {
|
|
80
|
+
res.json({ status: 'ok' });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// WebSocket server
|
|
84
|
+
const wss = new WebSocketServer({ server, path: `/ws/${token}` });
|
|
85
|
+
|
|
86
|
+
wss.on('connection', (ws) => {
|
|
87
|
+
console.log('WebSocket client connected');
|
|
88
|
+
hasActiveConnection = true;
|
|
89
|
+
updateActivity();
|
|
90
|
+
|
|
91
|
+
// Start exit timer on first connection
|
|
92
|
+
startExitTimer();
|
|
93
|
+
|
|
94
|
+
let ptyProcess = null;
|
|
95
|
+
let authenticated = false;
|
|
96
|
+
|
|
97
|
+
// Send auth request
|
|
98
|
+
ws.send(JSON.stringify({ type: 'auth_required' }));
|
|
99
|
+
|
|
100
|
+
// Handle incoming WebSocket messages
|
|
101
|
+
ws.on('message', (data) => {
|
|
102
|
+
const message = data.toString();
|
|
103
|
+
|
|
104
|
+
// Handle authentication
|
|
105
|
+
if (!authenticated) {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(message);
|
|
108
|
+
if (parsed.type === 'auth' && parsed.password) {
|
|
109
|
+
if (parsed.password === password) {
|
|
110
|
+
authenticated = true;
|
|
111
|
+
updateActivity();
|
|
112
|
+
console.log('Client authenticated successfully');
|
|
113
|
+
|
|
114
|
+
// Send auth success with config
|
|
115
|
+
ws.send(JSON.stringify({
|
|
116
|
+
type: 'auth_success',
|
|
117
|
+
config: {
|
|
118
|
+
lockTimeout: lockTimeout, // minutes
|
|
119
|
+
exitTimeout: exitTimeout // minutes
|
|
120
|
+
}
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
// Now spawn the PTY
|
|
124
|
+
try {
|
|
125
|
+
const tmuxBin = getTmuxPath() || 'tmux';
|
|
126
|
+
console.log(`Spawning PTY: ${tmuxBin} attach-session -t ${session}`);
|
|
127
|
+
|
|
128
|
+
ptyProcess = pty.spawn(tmuxBin, ['attach-session', '-t', session], {
|
|
129
|
+
name: 'xterm-256color',
|
|
130
|
+
cols: parsed.cols || 80,
|
|
131
|
+
rows: parsed.rows || 24,
|
|
132
|
+
cwd: process.env.HOME,
|
|
133
|
+
env: {
|
|
134
|
+
...process.env,
|
|
135
|
+
TERM: 'xterm-256color'
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Pipe PTY output to WebSocket
|
|
140
|
+
ptyProcess.onData((ptyData) => {
|
|
141
|
+
try {
|
|
142
|
+
if (ws.readyState === ws.OPEN) {
|
|
143
|
+
ws.send(ptyData);
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error('Error sending to WebSocket:', err);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
151
|
+
console.log(`PTY exited with code ${exitCode}, signal ${signal}`);
|
|
152
|
+
if (ws.readyState === ws.OPEN) {
|
|
153
|
+
ws.close();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error('Failed to spawn PTY:', err.message);
|
|
158
|
+
ws.send(`\r\n\x1b[31mError: Failed to attach to tmux session '${session}'\x1b[0m\r\n`);
|
|
159
|
+
ws.close(1011, 'PTY spawn failed');
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
console.log('Client authentication failed');
|
|
163
|
+
ws.send(JSON.stringify({ type: 'auth_failed' }));
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// Not JSON or not auth message
|
|
169
|
+
}
|
|
170
|
+
return; // Ignore all messages until authenticated
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Already authenticated - handle messages
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(message);
|
|
176
|
+
|
|
177
|
+
// Handle activity ping (resets server-side timer)
|
|
178
|
+
if (parsed.type === 'activity') {
|
|
179
|
+
updateActivity();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Handle resize command
|
|
184
|
+
if (parsed.type === 'resize' && parsed.cols && parsed.rows) {
|
|
185
|
+
updateActivity();
|
|
186
|
+
if (ptyProcess) {
|
|
187
|
+
ptyProcess.resize(parsed.cols, parsed.rows);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Handle re-auth (after lock screen)
|
|
193
|
+
if (parsed.type === 're-auth' && parsed.password) {
|
|
194
|
+
if (parsed.password === password) {
|
|
195
|
+
updateActivity();
|
|
196
|
+
ws.send(JSON.stringify({ type: 'auth_success' }));
|
|
197
|
+
} else {
|
|
198
|
+
ws.send(JSON.stringify({ type: 'auth_failed' }));
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Handle full cleanup request
|
|
204
|
+
if (parsed.type === 'cleanup') {
|
|
205
|
+
console.log('\nFull cleanup requested from web client...');
|
|
206
|
+
|
|
207
|
+
// Kill PTY first
|
|
208
|
+
if (ptyProcess) {
|
|
209
|
+
ptyProcess.kill();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Kill tmux session
|
|
213
|
+
killSession(session);
|
|
214
|
+
console.log(`Killed tmux session: ${session}`);
|
|
215
|
+
|
|
216
|
+
// Notify client
|
|
217
|
+
ws.send(JSON.stringify({ type: 'cleanup_complete' }));
|
|
218
|
+
|
|
219
|
+
// Close all WebSocket connections
|
|
220
|
+
wss.clients.forEach(client => {
|
|
221
|
+
if (client.readyState === client.OPEN) {
|
|
222
|
+
client.close(1000, 'Cleanup requested');
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Stop exit timer
|
|
227
|
+
if (exitTimer) clearInterval(exitTimer);
|
|
228
|
+
|
|
229
|
+
// Give time for cleanup message to send, then exit
|
|
230
|
+
setTimeout(() => {
|
|
231
|
+
console.log('Server shutting down...');
|
|
232
|
+
server.close();
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}, 500);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
// Not JSON - treat as terminal input
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Terminal input
|
|
242
|
+
if (ptyProcess) {
|
|
243
|
+
updateActivity();
|
|
244
|
+
ptyProcess.write(message);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
ws.on('close', () => {
|
|
249
|
+
console.log('WebSocket client disconnected');
|
|
250
|
+
if (ptyProcess) {
|
|
251
|
+
ptyProcess.kill();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check if any connections remain
|
|
255
|
+
hasActiveConnection = wss.clients.size > 0;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
ws.on('error', (err) => {
|
|
259
|
+
console.error('WebSocket error:', err);
|
|
260
|
+
if (ptyProcess) {
|
|
261
|
+
ptyProcess.kill();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Handle upgrade requests - only allow valid token
|
|
267
|
+
server.on('upgrade', (request, socket, head) => {
|
|
268
|
+
const expectedPath = `/ws/${token}`;
|
|
269
|
+
if (request.url !== expectedPath) {
|
|
270
|
+
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
271
|
+
socket.destroy();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
server.listen(port, () => {
|
|
277
|
+
resolve(server);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
server.on('error', (err) => {
|
|
281
|
+
reject(err);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
package/lib/tmux.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { execSync, spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
// Cache the tmux path
|
|
4
|
+
let tmuxPath = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the full path to tmux binary
|
|
8
|
+
*/
|
|
9
|
+
export function getTmuxPath() {
|
|
10
|
+
if (tmuxPath) return tmuxPath;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
tmuxPath = execSync('which tmux', { encoding: 'utf-8' }).trim();
|
|
14
|
+
return tmuxPath;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if tmux is installed
|
|
22
|
+
*/
|
|
23
|
+
export function isTmuxInstalled() {
|
|
24
|
+
return getTmuxPath() !== null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if a tmux session exists
|
|
29
|
+
*/
|
|
30
|
+
export function hasSession(name) {
|
|
31
|
+
const tmuxBin = getTmuxPath() || 'tmux';
|
|
32
|
+
try {
|
|
33
|
+
execSync(`${tmuxBin} has-session -t ${name} 2>/dev/null`);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a new tmux session running a command with the given args
|
|
42
|
+
* @param {string} name - tmux session name
|
|
43
|
+
* @param {string} command - command to run (e.g., 'claude', 'agent')
|
|
44
|
+
* @param {string[]} args - arguments to pass to the command
|
|
45
|
+
*/
|
|
46
|
+
export function createSession(name, command, args = []) {
|
|
47
|
+
const tmuxBin = getTmuxPath() || 'tmux';
|
|
48
|
+
const cmd = [command, ...args].join(' ');
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// Create detached tmux session running the command
|
|
52
|
+
execSync(`${tmuxBin} new-session -d -s ${name} '${cmd}'`, {
|
|
53
|
+
stdio: 'inherit'
|
|
54
|
+
});
|
|
55
|
+
} catch (err) {
|
|
56
|
+
throw new Error(`Failed to create tmux session: ${err.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Kill a tmux session
|
|
62
|
+
*/
|
|
63
|
+
export function killSession(name) {
|
|
64
|
+
const tmuxBin = getTmuxPath() || 'tmux';
|
|
65
|
+
try {
|
|
66
|
+
execSync(`${tmuxBin} kill-session -t ${name} 2>/dev/null`);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* List all tmux sessions
|
|
75
|
+
*/
|
|
76
|
+
export function listSessions() {
|
|
77
|
+
const tmuxBin = getTmuxPath() || 'tmux';
|
|
78
|
+
try {
|
|
79
|
+
const output = execSync(`${tmuxBin} list-sessions -F "#{session_name}"`, {
|
|
80
|
+
encoding: 'utf-8'
|
|
81
|
+
});
|
|
82
|
+
return output.trim().split('\n').filter(Boolean);
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
package/lib/tunnel.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { spawn, execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if ngrok is already running
|
|
5
|
+
* Returns { running: boolean, pids: number[] }
|
|
6
|
+
*/
|
|
7
|
+
export function checkNgrokRunning() {
|
|
8
|
+
try {
|
|
9
|
+
let output;
|
|
10
|
+
if (process.platform === 'win32') {
|
|
11
|
+
output = execSync('tasklist /FI "IMAGENAME eq ngrok.exe" /FO CSV /NH 2>nul', { encoding: 'utf8' });
|
|
12
|
+
const lines = output.trim().split('\n').filter(l => l.includes('ngrok.exe'));
|
|
13
|
+
if (lines.length > 0) {
|
|
14
|
+
// Extract PIDs from CSV format: "ngrok.exe","1234",...
|
|
15
|
+
const pids = lines.map(l => {
|
|
16
|
+
const match = l.match(/"ngrok\.exe","(\d+)"/);
|
|
17
|
+
return match ? parseInt(match[1], 10) : null;
|
|
18
|
+
}).filter(Boolean);
|
|
19
|
+
return { running: true, pids };
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
output = execSync('pgrep -f "ngrok http" 2>/dev/null || true', { encoding: 'utf8' });
|
|
23
|
+
const pids = output.trim().split('\n').filter(Boolean).map(p => parseInt(p, 10));
|
|
24
|
+
if (pids.length > 0) {
|
|
25
|
+
return { running: true, pids };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
// pgrep returns non-zero if no process found, which is fine
|
|
30
|
+
}
|
|
31
|
+
return { running: false, pids: [] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Start a tunnel (ngrok or cloudflared) and return the public URL
|
|
36
|
+
*/
|
|
37
|
+
export async function startTunnel(type, port) {
|
|
38
|
+
if (type === 'ngrok') {
|
|
39
|
+
return startNgrok(port);
|
|
40
|
+
} else if (type === 'cloudflared') {
|
|
41
|
+
return startCloudflared(port);
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error(`Unknown tunnel type: ${type}. Use 'ngrok' or 'cloudflared'`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Start ngrok and extract the public URL
|
|
49
|
+
*/
|
|
50
|
+
function startNgrok(port) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const proc = spawn('ngrok', ['http', port.toString(), '--log', 'stdout'], {
|
|
53
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let output = '';
|
|
57
|
+
let resolved = false;
|
|
58
|
+
|
|
59
|
+
proc.stdout.on('data', (data) => {
|
|
60
|
+
output += data.toString();
|
|
61
|
+
|
|
62
|
+
// Look for the URL in ngrok output
|
|
63
|
+
// Format: url=https://xxxx.ngrok.io or url=https://xxxx.ngrok-free.app
|
|
64
|
+
const urlMatch = output.match(/url=(https:\/\/[^\s]+)/);
|
|
65
|
+
if (urlMatch && !resolved) {
|
|
66
|
+
resolved = true;
|
|
67
|
+
resolve({
|
|
68
|
+
url: urlMatch[1],
|
|
69
|
+
kill: () => proc.kill()
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
proc.stderr.on('data', (data) => {
|
|
75
|
+
output += data.toString();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
proc.on('error', (err) => {
|
|
79
|
+
if (!resolved) {
|
|
80
|
+
reject(new Error(`Failed to start ngrok: ${err.message}. Is ngrok installed?`));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
proc.on('exit', (code) => {
|
|
85
|
+
if (!resolved) {
|
|
86
|
+
reject(new Error(`ngrok exited with code ${code}. Output: ${output}`));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Timeout after 30 seconds
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
if (!resolved) {
|
|
93
|
+
proc.kill();
|
|
94
|
+
reject(new Error('Timeout waiting for ngrok URL'));
|
|
95
|
+
}
|
|
96
|
+
}, 30000);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Start cloudflared and extract the public URL
|
|
102
|
+
*/
|
|
103
|
+
function startCloudflared(port) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const proc = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], {
|
|
106
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
let output = '';
|
|
110
|
+
let resolved = false;
|
|
111
|
+
|
|
112
|
+
const handleOutput = (data) => {
|
|
113
|
+
output += data.toString();
|
|
114
|
+
|
|
115
|
+
// Look for the URL in cloudflared output
|
|
116
|
+
// Format: https://xxxx.trycloudflare.com
|
|
117
|
+
const urlMatch = output.match(/(https:\/\/[^\s]+\.trycloudflare\.com)/);
|
|
118
|
+
if (urlMatch && !resolved) {
|
|
119
|
+
resolved = true;
|
|
120
|
+
resolve({
|
|
121
|
+
url: urlMatch[1],
|
|
122
|
+
kill: () => proc.kill()
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
proc.stdout.on('data', handleOutput);
|
|
128
|
+
proc.stderr.on('data', handleOutput); // cloudflared outputs to stderr
|
|
129
|
+
|
|
130
|
+
proc.on('error', (err) => {
|
|
131
|
+
if (!resolved) {
|
|
132
|
+
reject(new Error(`Failed to start cloudflared: ${err.message}. Is cloudflared installed?`));
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
proc.on('exit', (code) => {
|
|
137
|
+
if (!resolved) {
|
|
138
|
+
reject(new Error(`cloudflared exited with code ${code}. Output: ${output}`));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Timeout after 30 seconds
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
if (!resolved) {
|
|
145
|
+
proc.kill();
|
|
146
|
+
reject(new Error('Timeout waiting for cloudflared URL'));
|
|
147
|
+
}
|
|
148
|
+
}, 30000);
|
|
149
|
+
});
|
|
150
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aigo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Stream Claude Code to the web - run Claude remotely from any device",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aigo": "./bin/vigo.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/vigo.js",
|
|
11
|
+
"postinstall": "chmod +x node_modules/node-pty/prebuilds/darwin-*/spawn-helper 2>/dev/null || true"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude",
|
|
15
|
+
"terminal",
|
|
16
|
+
"web",
|
|
17
|
+
"tmux",
|
|
18
|
+
"remote"
|
|
19
|
+
],
|
|
20
|
+
"author": "wakandan221",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"express": "^4.21.0",
|
|
24
|
+
"ws": "^8.18.0",
|
|
25
|
+
"node-pty": "^1.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|