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/bin/vigo.js ADDED
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseArgs } from '../lib/cli.js';
4
+ import { isTmuxInstalled, hasSession, createSession } from '../lib/tmux.js';
5
+ import { isAgentInstalled, checkAgentAuth } from '../lib/cursor.js';
6
+ import { startServer } from '../lib/server.js';
7
+ import { startTunnel, checkNgrokRunning } from '../lib/tunnel.js';
8
+ import { findAvailablePort } from '../lib/port.js';
9
+ import { config } from '../lib/config.js';
10
+ import crypto from 'crypto';
11
+
12
+ const VERSION = '1.2.0';
13
+
14
+ function printBanner(info) {
15
+ const accessUrl = info.tunnelUrl
16
+ ? `${info.tunnelUrl}/t/${info.token}`
17
+ : `${info.localUrl}/t/${info.token}`;
18
+
19
+ const lockStr = info.lockTimeout > 0 ? `${info.lockTimeout}min` : 'off';
20
+ const exitStr = info.exitTimeout > 0 ? `${info.exitTimeout}min` : 'off';
21
+ const timeoutStr = `lock:${lockStr} exit:${exitStr}`;
22
+
23
+ const toolTitle = info.tool === 'cursor' ? 'Cursor Agent' : 'Claude Code';
24
+ const header = `vigo - ${toolTitle} Web Terminal`;
25
+
26
+ console.log('');
27
+ console.log('┌────────────────────────────────────────┐');
28
+ console.log(`│ ${header.padEnd(37)}│`);
29
+ console.log('├────────────────────────────────────────┤');
30
+ console.log(`│ Session: ${info.session.padEnd(27)}│`);
31
+ console.log(`│ Local: ${info.localUrl.padEnd(27)}│`);
32
+ if (info.tunnelUrl) {
33
+ console.log(`│ Tunnel: ${info.tunnelUrl.padEnd(27)}│`);
34
+ }
35
+ console.log(`│ Password: ${info.password.padEnd(27)}│`);
36
+ console.log(`│ Timeout: ${timeoutStr.padEnd(27)}│`);
37
+ console.log('└────────────────────────────────────────┘');
38
+ console.log('');
39
+ console.log('Access URL:');
40
+ console.log(` ${accessUrl}`);
41
+ console.log('');
42
+ console.log('Press Ctrl+C to stop (tmux session persists)');
43
+ console.log('');
44
+ }
45
+
46
+ function printHelp() {
47
+ console.log(`
48
+ vigo v${VERSION} - Stream Claude Code or Cursor Agent to the web
49
+
50
+ Usage:
51
+ vigo [options] claude [claude-args]
52
+ vigo [options] cursor [cursor-args]
53
+ vigo [options] agent [agent-args]
54
+ vigo [options] --attach <session>
55
+
56
+ Tools:
57
+ claude Use Claude Code CLI
58
+ cursor, agent Use Cursor Agent CLI
59
+
60
+ Options:
61
+ --port, -p <port> Web server port (default: ${config.defaultPort}, auto-selects if busy)
62
+ --session, -s <name> tmux session name (default: claude-code or cursor-agent)
63
+ --tunnel, -t <type> Tunnel: ngrok, cloudflared, none (default: ${config.defaultTunnel})
64
+ --attach, -a <session> Attach to existing tmux session (don't start new)
65
+ --password, -P <pass> Custom password (default: auto-generated ${config.passwordLength}-char)
66
+ --timeout, -T <mins> Lock screen after inactivity (default: disabled)
67
+ --exit-timeout, -E <mins> Exit session after inactivity (default: disabled)
68
+ --help, -h Show this help message
69
+ --version, -v Show version
70
+
71
+ Examples:
72
+ vigo claude Start Claude Code with ngrok tunnel
73
+ vigo cursor Start Cursor Agent with ngrok tunnel
74
+ vigo --tunnel none claude Local only (no tunnel)
75
+ vigo -p 8080 -s myproject cursor Custom port and session
76
+ vigo claude --model sonnet Pass args to Claude
77
+ vigo cursor --model gpt-5 Pass args to Cursor Agent
78
+ vigo --attach myproject Attach to existing session
79
+ vigo -P mypass123 cursor Custom password
80
+ `);
81
+ }
82
+
83
+ async function main() {
84
+ const { vigoArgs, toolArgs, tool } = parseArgs(process.argv.slice(2));
85
+
86
+ // Handle help and version
87
+ if (vigoArgs.help) {
88
+ printHelp();
89
+ process.exit(0);
90
+ }
91
+
92
+ if (vigoArgs.version) {
93
+ console.log(`vigo v${VERSION}`);
94
+ process.exit(0);
95
+ }
96
+
97
+ // Check if tmux is installed
98
+ if (!isTmuxInstalled()) {
99
+ console.error('Error: tmux is not installed or not in PATH');
100
+ console.error('');
101
+ console.error('Install tmux:');
102
+ console.error(' macOS: brew install tmux');
103
+ console.error(' Ubuntu: sudo apt install tmux');
104
+ console.error(' Arch: sudo pacman -S tmux');
105
+ process.exit(1);
106
+ }
107
+
108
+ // Determine which tool to use (from config default)
109
+ const activeTool = tool || config.defaultTool;
110
+ const toolConfig = config.tools[activeTool];
111
+
112
+ // Check if the tool CLI is installed (for non-attach mode)
113
+ if (!vigoArgs.attach && activeTool === 'cursor') {
114
+ if (!isAgentInstalled()) {
115
+ console.error('Error: Cursor Agent CLI is not installed or not in PATH');
116
+ console.error('');
117
+ console.error('Install Cursor CLI:');
118
+ console.error(' curl https://cursor.com/install -fsS | bash');
119
+ console.error('');
120
+ console.error('Then authenticate:');
121
+ console.error(' agent login');
122
+ process.exit(1);
123
+ }
124
+
125
+ // Check authentication
126
+ const authStatus = checkAgentAuth();
127
+ if (!authStatus.authenticated) {
128
+ console.error('Warning: Cursor Agent may not be authenticated');
129
+ console.error(` ${authStatus.message}`);
130
+ console.error('');
131
+ console.error('To authenticate:');
132
+ console.error(' agent login');
133
+ console.error(' # or set CURSOR_API_KEY environment variable');
134
+ console.error('');
135
+ }
136
+ }
137
+
138
+ const sessionName = vigoArgs.session || toolConfig.defaultSession;
139
+ const requestedPort = vigoArgs.port || config.defaultPort;
140
+ const tunnelType = vigoArgs.tunnel || config.defaultTunnel;
141
+
142
+ // Find available port
143
+ let port;
144
+ try {
145
+ port = await findAvailablePort(requestedPort);
146
+ if (port !== requestedPort) {
147
+ console.log(`⚠ Port ${requestedPort} in use, using port ${port} instead`);
148
+ }
149
+ } catch (err) {
150
+ console.error(`Error: ${err.message}`);
151
+ process.exit(1);
152
+ }
153
+
154
+ // Generate secure token
155
+ const token = crypto.randomBytes(config.tokenBytes).toString('hex');
156
+
157
+ // Use custom password or generate password from config
158
+ let password;
159
+ if (vigoArgs.password) {
160
+ password = vigoArgs.password;
161
+ } else {
162
+ password = '';
163
+ const randomBytes = crypto.randomBytes(config.passwordLength);
164
+ for (let i = 0; i < config.passwordLength; i++) {
165
+ password += config.passwordChars[randomBytes[i] % config.passwordChars.length];
166
+ }
167
+ }
168
+
169
+ // Timeout settings (in minutes) - use config defaults
170
+ const lockTimeout = vigoArgs.timeout !== null ? vigoArgs.timeout : config.lockTimeout;
171
+ const exitTimeout = vigoArgs.exitTimeout !== null ? vigoArgs.exitTimeout : config.exitTimeout;
172
+
173
+ // Handle --attach mode
174
+ if (vigoArgs.attach) {
175
+ const attachSession = typeof vigoArgs.attach === 'string' ? vigoArgs.attach : sessionName;
176
+ if (!hasSession(attachSession)) {
177
+ console.error(`Error: tmux session '${attachSession}' does not exist`);
178
+ console.error(`Create it first with: tmux new-session -s ${attachSession}`);
179
+ process.exit(1);
180
+ }
181
+ console.log(`✓ Attaching to existing tmux session: ${attachSession}`);
182
+ } else {
183
+ // Check if session already exists
184
+ if (hasSession(sessionName)) {
185
+ console.log(`✓ Using existing tmux session: ${sessionName}`);
186
+ } else {
187
+ // Create new session with the selected tool
188
+ console.log(`✓ Creating tmux session: ${sessionName} (${toolConfig.name})`);
189
+ createSession(sessionName, toolConfig.command, toolArgs);
190
+ }
191
+ }
192
+
193
+ const actualSession = vigoArgs.attach || sessionName;
194
+
195
+ // Start web server
196
+ console.log(`✓ Starting web server on port ${port}...`);
197
+ const server = await startServer({
198
+ port,
199
+ token,
200
+ password,
201
+ session: actualSession,
202
+ lockTimeout,
203
+ exitTimeout
204
+ });
205
+
206
+ // Start tunnel if requested (ngrok is now default)
207
+ let tunnelUrl = null;
208
+ let tunnel = null;
209
+ if (tunnelType !== 'none') {
210
+ // Check for existing ngrok process before starting
211
+ if (tunnelType === 'ngrok') {
212
+ const ngrokStatus = checkNgrokRunning();
213
+ if (ngrokStatus.running) {
214
+ console.error('');
215
+ console.error('Error: An ngrok process is already running.');
216
+ console.error('Free ngrok accounts only allow 1 simultaneous tunnel.');
217
+ console.error('');
218
+ console.error('To kill the existing ngrok process, run:');
219
+ if (process.platform === 'win32') {
220
+ console.error(` taskkill /F /PID ${ngrokStatus.pids.join(' /PID ')}`);
221
+ } else {
222
+ console.error(` kill ${ngrokStatus.pids.join(' ')}`);
223
+ }
224
+ console.error('');
225
+ console.error('Or to run without a tunnel:');
226
+ console.error(' vigo --tunnel none cursor');
227
+ console.error('');
228
+ process.exit(1);
229
+ }
230
+ }
231
+
232
+ console.log(`✓ Starting ${tunnelType} tunnel...`);
233
+ try {
234
+ tunnel = await startTunnel(tunnelType, port);
235
+ tunnelUrl = tunnel.url;
236
+ } catch (err) {
237
+ console.error(`Warning: Failed to start ${tunnelType} tunnel: ${err.message}`);
238
+ console.error('Continuing without tunnel...');
239
+ }
240
+ }
241
+
242
+ // Print access info
243
+ printBanner({
244
+ session: actualSession,
245
+ localUrl: `http://localhost:${port}`,
246
+ tunnelUrl,
247
+ token,
248
+ password,
249
+ lockTimeout,
250
+ exitTimeout,
251
+ tool: activeTool
252
+ });
253
+
254
+ // Keep process alive and handle cleanup
255
+ process.on('SIGINT', () => {
256
+ console.log('\nShutting down server...');
257
+
258
+ // Kill tunnel if running
259
+ if (tunnel) {
260
+ tunnel.kill();
261
+ }
262
+
263
+ console.log(`tmux session '${actualSession}' is still running.`);
264
+ console.log(`Re-attach with: vigo --attach ${actualSession}`);
265
+ console.log(`Or directly: tmux attach -t ${actualSession}`);
266
+ server.close();
267
+ process.exit(0);
268
+ });
269
+
270
+ process.on('SIGTERM', () => {
271
+ if (tunnel) {
272
+ tunnel.kill();
273
+ }
274
+ server.close();
275
+ process.exit(0);
276
+ });
277
+ }
278
+
279
+ main().catch(err => {
280
+ console.error('Error:', err.message);
281
+ process.exit(1);
282
+ });
package/lib/cli.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Parse command line arguments, separating vigo args from tool args
3
+ *
4
+ * Example: vigo --port 8080 --tunnel ngrok claude --model sonnet
5
+ * Example: vigo --port 8080 cursor --model gpt-5
6
+ * Returns:
7
+ * vigoArgs: { port: 8080, tunnel: 'ngrok' }
8
+ * toolArgs: ['--model', 'sonnet']
9
+ * tool: 'claude' | 'cursor' | null
10
+ */
11
+ export function parseArgs(args) {
12
+ const vigoArgs = {
13
+ port: null,
14
+ session: null,
15
+ tunnel: null,
16
+ attach: null,
17
+ password: null,
18
+ timeout: null, // Lock screen timeout in minutes (default: 15)
19
+ exitTimeout: null, // Exit after inactivity in minutes (default: 30)
20
+ help: false,
21
+ version: false
22
+ };
23
+
24
+ let toolArgs = [];
25
+ let tool = null;
26
+ let i = 0;
27
+
28
+ // Supported tool commands
29
+ const TOOL_COMMANDS = ['claude', 'cursor', 'agent'];
30
+
31
+ while (i < args.length) {
32
+ const arg = args[i];
33
+
34
+ // Once we hit a tool command, everything after is tool args
35
+ if (TOOL_COMMANDS.includes(arg)) {
36
+ // Normalize 'agent' to 'cursor' for consistency
37
+ tool = arg === 'agent' ? 'cursor' : arg;
38
+ toolArgs = args.slice(i + 1);
39
+ break;
40
+ }
41
+
42
+ // Parse vigo options
43
+ if (arg === '--port' || arg === '-p') {
44
+ vigoArgs.port = parseInt(args[++i], 10);
45
+ } else if (arg === '--session' || arg === '-s') {
46
+ vigoArgs.session = args[++i];
47
+ } else if (arg === '--tunnel' || arg === '-t') {
48
+ vigoArgs.tunnel = args[++i];
49
+ } else if (arg === '--attach' || arg === '-a') {
50
+ const next = args[i + 1];
51
+ // Check if next arg is a session name (not another flag)
52
+ if (next && !next.startsWith('-')) {
53
+ vigoArgs.attach = args[++i];
54
+ } else {
55
+ vigoArgs.attach = true; // Use default session name
56
+ }
57
+ } else if (arg === '--password' || arg === '-P') {
58
+ vigoArgs.password = args[++i];
59
+ } else if (arg === '--timeout' || arg === '-T') {
60
+ vigoArgs.timeout = parseInt(args[++i], 10);
61
+ } else if (arg === '--exit-timeout' || arg === '-E') {
62
+ vigoArgs.exitTimeout = parseInt(args[++i], 10);
63
+ } else if (arg === '--help' || arg === '-h') {
64
+ vigoArgs.help = true;
65
+ } else if (arg === '--version' || arg === '-v') {
66
+ vigoArgs.version = true;
67
+ }
68
+
69
+ i++;
70
+ }
71
+
72
+ // For backward compatibility, also export as claudeArgs/foundClaude
73
+ return {
74
+ vigoArgs,
75
+ toolArgs,
76
+ tool,
77
+ // Backward compatibility
78
+ claudeArgs: toolArgs,
79
+ foundClaude: tool === 'claude'
80
+ };
81
+ }
package/lib/config.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * vigo configuration
3
+ *
4
+ * Edit these values to customize vigo's default behavior.
5
+ */
6
+
7
+ export const config = {
8
+ // ============================================
9
+ // Server Settings
10
+ // ============================================
11
+
12
+ /** Default web server port (auto-selects next available if busy) */
13
+ defaultPort: 3000,
14
+
15
+ /** Default tunnel type: 'ngrok', 'cloudflared', or 'none' */
16
+ defaultTunnel: 'ngrok',
17
+
18
+ // ============================================
19
+ // Session Timeouts
20
+ // ============================================
21
+
22
+ /**
23
+ * Lock screen timeout in minutes (0 = disabled)
24
+ * After this period of inactivity, the session locks and requires password re-entry
25
+ * Default: 0 (disabled) for persistent sessions
26
+ */
27
+ lockTimeout: 0,
28
+
29
+ /**
30
+ * Exit timeout in minutes (0 = disabled)
31
+ * After this period of total inactivity, the session terminates completely
32
+ * Default: 0 (disabled) for persistent sessions
33
+ */
34
+ exitTimeout: 0,
35
+
36
+ // ============================================
37
+ // Tool Configurations
38
+ // ============================================
39
+
40
+ tools: {
41
+ claude: {
42
+ name: 'Claude Code',
43
+ command: 'claude',
44
+ defaultSession: 'claude-code'
45
+ },
46
+ cursor: {
47
+ name: 'Cursor Agent',
48
+ command: 'agent',
49
+ defaultSession: 'cursor-agent'
50
+ }
51
+ },
52
+
53
+ /** Default tool when none specified */
54
+ defaultTool: 'claude',
55
+
56
+ // ============================================
57
+ // Security Settings
58
+ // ============================================
59
+
60
+ /** Length of auto-generated password */
61
+ passwordLength: 6,
62
+
63
+ /** Characters used for auto-generated passwords (no ambiguous chars like 0/O, 1/l/I) */
64
+ passwordChars: 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789',
65
+
66
+ /** Length of URL token (in bytes, will be hex encoded = 2x chars) */
67
+ tokenBytes: 2,
68
+
69
+ // ============================================
70
+ // Reconnection Settings
71
+ // ============================================
72
+
73
+ /** Maximum reconnection attempts before giving up */
74
+ maxReconnectAttempts: 10,
75
+
76
+ /** Base delay between reconnection attempts (ms) */
77
+ reconnectDelay: 2000
78
+ };
79
+
80
+ export default config;
package/lib/cursor.js ADDED
@@ -0,0 +1,74 @@
1
+ import { execSync } from 'child_process';
2
+
3
+ // Cache the agent path
4
+ let agentPath = null;
5
+
6
+ /**
7
+ * Get the full path to Cursor agent CLI binary
8
+ */
9
+ export function getAgentPath() {
10
+ if (agentPath) return agentPath;
11
+
12
+ try {
13
+ agentPath = execSync('which agent', { encoding: 'utf-8' }).trim();
14
+ return agentPath;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Check if Cursor agent CLI is installed
22
+ */
23
+ export function isAgentInstalled() {
24
+ return getAgentPath() !== null;
25
+ }
26
+
27
+ /**
28
+ * Check if Cursor CLI is authenticated
29
+ * Returns: { authenticated: boolean, message: string }
30
+ */
31
+ export function checkAgentAuth() {
32
+ const agentBin = getAgentPath();
33
+ if (!agentBin) {
34
+ return {
35
+ authenticated: false,
36
+ message: 'Cursor agent CLI is not installed'
37
+ };
38
+ }
39
+
40
+ try {
41
+ // Try to check auth status
42
+ const output = execSync(`${agentBin} status 2>&1`, {
43
+ encoding: 'utf-8',
44
+ timeout: 10000
45
+ });
46
+
47
+ // Check for common authenticated patterns
48
+ if (output.includes('logged in') || output.includes('authenticated')) {
49
+ return { authenticated: true, message: 'Authenticated' };
50
+ }
51
+
52
+ return {
53
+ authenticated: false,
54
+ message: 'Not authenticated. Run: agent login'
55
+ };
56
+ } catch (err) {
57
+ // If CURSOR_API_KEY is set, assume authenticated
58
+ if (process.env.CURSOR_API_KEY) {
59
+ return { authenticated: true, message: 'Using CURSOR_API_KEY' };
60
+ }
61
+
62
+ return {
63
+ authenticated: false,
64
+ message: 'Unable to verify auth status. Try: agent login'
65
+ };
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Get the command to run for Cursor CLI
71
+ */
72
+ export function getAgentCommand() {
73
+ return getAgentPath() || 'agent';
74
+ }
package/lib/port.js ADDED
@@ -0,0 +1,39 @@
1
+ import { createServer } from 'net';
2
+
3
+ /**
4
+ * Check if a port is available
5
+ */
6
+ export function isPortAvailable(port) {
7
+ return new Promise((resolve) => {
8
+ const server = createServer();
9
+
10
+ server.once('error', (err) => {
11
+ if (err.code === 'EADDRINUSE') {
12
+ resolve(false);
13
+ } else {
14
+ resolve(false);
15
+ }
16
+ });
17
+
18
+ server.once('listening', () => {
19
+ server.close();
20
+ resolve(true);
21
+ });
22
+
23
+ server.listen(port, '127.0.0.1');
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Find an available port starting from the given port
29
+ * Tries up to maxAttempts ports
30
+ */
31
+ export async function findAvailablePort(startPort, maxAttempts = 100) {
32
+ for (let i = 0; i < maxAttempts; i++) {
33
+ const port = startPort + i;
34
+ if (await isPortAvailable(port)) {
35
+ return port;
36
+ }
37
+ }
38
+ throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${startPort}`);
39
+ }