vcode-cli 1.0.1 → 1.0.3
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/vcode.js +19 -0
- package/lib/bridge.js +41 -28
- package/lib/commands/start.js +55 -16
- package/lib/server.js +410 -0
- package/package.json +3 -2
package/bin/vcode.js
CHANGED
|
@@ -33,11 +33,30 @@ program
|
|
|
33
33
|
.command('start')
|
|
34
34
|
.description('Start the V Code bridge and connect to Vynthen')
|
|
35
35
|
.option('--approve-all', 'Auto-approve all operations for this session')
|
|
36
|
+
.option('--server', 'Auto-start the WebSocket server (default: true)', true)
|
|
37
|
+
.option('--local', 'Use local WebSocket server (default, use --no-local for remote)', true)
|
|
36
38
|
.action(async (opts) => {
|
|
39
|
+
if (!opts.server) {
|
|
40
|
+
process.env.VCODE_WS_URL = 'ws://localhost:3001';
|
|
41
|
+
}
|
|
37
42
|
const { start } = await import('../lib/commands/start.js');
|
|
38
43
|
await start(opts);
|
|
39
44
|
});
|
|
40
45
|
|
|
46
|
+
program
|
|
47
|
+
.command('serve')
|
|
48
|
+
.description('Start only the WebSocket server (no Vynthen connection)')
|
|
49
|
+
.option('--port <number>', 'Port to listen on', '3001')
|
|
50
|
+
.action(async (opts) => {
|
|
51
|
+
const { spawn } = await import('child_process');
|
|
52
|
+
const serverPath = new URL('../lib/server.js', import.meta.url);
|
|
53
|
+
const proc = spawn('node', [serverPath.pathname], {
|
|
54
|
+
stdio: 'inherit',
|
|
55
|
+
env: { ...process.env, VCODE_WS_PORT: opts.port }
|
|
56
|
+
});
|
|
57
|
+
proc.on('close', (code) => process.exit(code || 0));
|
|
58
|
+
});
|
|
59
|
+
|
|
41
60
|
program
|
|
42
61
|
.command('logout')
|
|
43
62
|
.description('Clear stored authentication token')
|
package/lib/bridge.js
CHANGED
|
@@ -10,11 +10,12 @@ import ora from 'ora';
|
|
|
10
10
|
import { dispatch } from './dispatcher.js';
|
|
11
11
|
import { getToken, isTokenExpired } from './keychain.js';
|
|
12
12
|
|
|
13
|
-
const WS_URL =
|
|
14
|
-
|
|
13
|
+
const WS_URL = process.env.VCODE_WS_URL ||
|
|
14
|
+
(process.env.VCODE_LOCAL === 'false' ? 'wss://vynthen.com/api/vcode-ws' : 'ws://localhost:3001');
|
|
15
|
+
const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 15000, 30000];
|
|
15
16
|
|
|
16
17
|
export class Bridge {
|
|
17
|
-
constructor({ onStatusChange, onOperation, onError }) {
|
|
18
|
+
constructor({ onStatusChange, onOperation, onError, onWorkingDirChange, onTerminalOutput }) {
|
|
18
19
|
this.ws = null;
|
|
19
20
|
this.token = null;
|
|
20
21
|
this.connected = false;
|
|
@@ -24,13 +25,13 @@ export class Bridge {
|
|
|
24
25
|
this.onStatusChange = onStatusChange || (() => {});
|
|
25
26
|
this.onOperation = onOperation || (() => {});
|
|
26
27
|
this.onError = onError || (() => {});
|
|
28
|
+
this.onWorkingDirChange = onWorkingDirChange || (() => {});
|
|
29
|
+
this.onTerminalOutput = onTerminalOutput || (() => {});
|
|
27
30
|
this.operationCount = 0;
|
|
28
31
|
this.startTime = null;
|
|
32
|
+
this.currentWorkingDir = process.cwd();
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
/**
|
|
32
|
-
* Start the bridge connection.
|
|
33
|
-
*/
|
|
34
35
|
async connect() {
|
|
35
36
|
this.token = await getToken();
|
|
36
37
|
|
|
@@ -47,9 +48,6 @@ export class Bridge {
|
|
|
47
48
|
this._connect();
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
/**
|
|
51
|
-
* Internal connect with reconnection logic.
|
|
52
|
-
*/
|
|
53
51
|
_connect() {
|
|
54
52
|
if (this.intentionalClose) return;
|
|
55
53
|
|
|
@@ -81,7 +79,7 @@ export class Bridge {
|
|
|
81
79
|
platform: process.platform,
|
|
82
80
|
arch: process.arch,
|
|
83
81
|
nodeVersion: process.version,
|
|
84
|
-
cwd:
|
|
82
|
+
cwd: this.currentWorkingDir,
|
|
85
83
|
hostname: os.hostname(),
|
|
86
84
|
username: os.userInfo().username,
|
|
87
85
|
homedir: os.homedir(),
|
|
@@ -134,9 +132,6 @@ export class Bridge {
|
|
|
134
132
|
});
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
/**
|
|
138
|
-
* Reconnect with exponential backoff.
|
|
139
|
-
*/
|
|
140
135
|
_reconnect() {
|
|
141
136
|
if (this.intentionalClose) return;
|
|
142
137
|
|
|
@@ -152,14 +147,12 @@ export class Bridge {
|
|
|
152
147
|
}, delay);
|
|
153
148
|
}
|
|
154
149
|
|
|
155
|
-
/**
|
|
156
|
-
* Handle incoming message from server.
|
|
157
|
-
*/
|
|
158
150
|
async _handleMessage(message) {
|
|
159
151
|
const { type, id, data } = message;
|
|
160
152
|
|
|
161
153
|
switch (type) {
|
|
162
|
-
case 'OPERATION':
|
|
154
|
+
case 'OPERATION':
|
|
155
|
+
case 'OPERATION_REQUEST': {
|
|
163
156
|
this.operationCount++;
|
|
164
157
|
const opType = data?.operation;
|
|
165
158
|
const params = data?.params || {};
|
|
@@ -194,6 +187,17 @@ export class Bridge {
|
|
|
194
187
|
break;
|
|
195
188
|
}
|
|
196
189
|
|
|
190
|
+
case 'TERMINAL_OUTPUT': {
|
|
191
|
+
this.onTerminalOutput(data);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case 'WORKING_DIR': {
|
|
196
|
+
this.currentWorkingDir = data.path;
|
|
197
|
+
this.onWorkingDirChange(data.path);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
197
201
|
case 'PING':
|
|
198
202
|
this.send({ type: 'PONG' });
|
|
199
203
|
break;
|
|
@@ -202,24 +206,25 @@ export class Bridge {
|
|
|
202
206
|
console.log(chalk.hex('#a855f7')(` ⓘ Server: ${data?.message || ''}`));
|
|
203
207
|
break;
|
|
204
208
|
|
|
209
|
+
case 'SET_WORKING_DIR': {
|
|
210
|
+
const newPath = data.path;
|
|
211
|
+
if (newPath) {
|
|
212
|
+
this.send({ type: 'SET_WORKING_DIR', path: newPath });
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
|
|
205
217
|
default:
|
|
206
|
-
// Unknown message type — ignore
|
|
207
218
|
break;
|
|
208
219
|
}
|
|
209
220
|
}
|
|
210
221
|
|
|
211
|
-
/**
|
|
212
|
-
* Send a message to the server.
|
|
213
|
-
*/
|
|
214
222
|
send(message) {
|
|
215
223
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
216
224
|
this.ws.send(JSON.stringify(message));
|
|
217
225
|
}
|
|
218
226
|
}
|
|
219
227
|
|
|
220
|
-
/**
|
|
221
|
-
* Gracefully disconnect.
|
|
222
|
-
*/
|
|
223
228
|
disconnect() {
|
|
224
229
|
this.intentionalClose = true;
|
|
225
230
|
if (this.ws) {
|
|
@@ -229,9 +234,6 @@ export class Bridge {
|
|
|
229
234
|
this.connected = false;
|
|
230
235
|
}
|
|
231
236
|
|
|
232
|
-
/**
|
|
233
|
-
* Get uptime in human-readable format.
|
|
234
|
-
*/
|
|
235
237
|
getUptime() {
|
|
236
238
|
if (!this.startTime) return '0s';
|
|
237
239
|
const diff = Date.now() - this.startTime;
|
|
@@ -242,4 +244,15 @@ export class Bridge {
|
|
|
242
244
|
if (mins > 0) return `${mins}m ${secs}s`;
|
|
243
245
|
return `${secs}s`;
|
|
244
246
|
}
|
|
245
|
-
|
|
247
|
+
|
|
248
|
+
getWorkingDir() {
|
|
249
|
+
return this.currentWorkingDir;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
setWorkingDir(path) {
|
|
253
|
+
this.currentWorkingDir = path;
|
|
254
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
255
|
+
this.send({ type: 'SET_WORKING_DIR', path });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
package/lib/commands/start.js
CHANGED
|
@@ -8,9 +8,11 @@ import { showLogo, startLogoAnimation } from '../logo.js';
|
|
|
8
8
|
import { getToken, isTokenExpired, getUser } from '../keychain.js';
|
|
9
9
|
import { Bridge } from '../bridge.js';
|
|
10
10
|
import { setSessionApproveAll } from '../permissions.js';
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
11
14
|
|
|
12
15
|
export async function start(opts = {}) {
|
|
13
|
-
// ── Pre-flight checks ──
|
|
14
16
|
const token = await getToken();
|
|
15
17
|
|
|
16
18
|
if (!token) {
|
|
@@ -23,21 +25,46 @@ export async function start(opts = {}) {
|
|
|
23
25
|
process.exit(1);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
// ── Session approve-all mode ──
|
|
27
28
|
if (opts.approveAll) {
|
|
28
29
|
setSessionApproveAll(true);
|
|
29
30
|
console.log(chalk.yellow('\n ⚠ Auto-approve mode enabled. All operations will be approved automatically.\n'));
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
// ── Show animated logo ──
|
|
33
33
|
const stopAnimation = startLogoAnimation();
|
|
34
|
-
|
|
35
|
-
// Let animation play for 2.5 seconds then stop for clean UI
|
|
36
34
|
await new Promise(resolve => setTimeout(resolve, 2500));
|
|
37
35
|
stopAnimation();
|
|
38
36
|
|
|
39
|
-
// Show connection info
|
|
40
37
|
const user = await getUser();
|
|
38
|
+
|
|
39
|
+
const serverUrl = process.env.VCODE_WS_URL ||
|
|
40
|
+
(process.env.VCODE_LOCAL === 'false' ? 'wss://vynthen.com/api/vcode-ws' : 'ws://localhost:3001');
|
|
41
|
+
const autoStartServer = opts.server !== false && opts.server !== undefined;
|
|
42
|
+
|
|
43
|
+
// Try to find the server script
|
|
44
|
+
const serverScript = path.join(process.cwd(), 'lib', 'server.js');
|
|
45
|
+
const serverExists = fs.existsSync(serverScript);
|
|
46
|
+
|
|
47
|
+
// Start the WebSocket server automatically if not already running
|
|
48
|
+
let serverProcess = null;
|
|
49
|
+
|
|
50
|
+
if (autoStartServer && serverExists) {
|
|
51
|
+
console.log(chalk.gray(' Starting V Code WebSocket server...'));
|
|
52
|
+
|
|
53
|
+
serverProcess = spawn('node', [serverScript], {
|
|
54
|
+
stdio: 'inherit',
|
|
55
|
+
detached: false
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Wait for server to start
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
60
|
+
|
|
61
|
+
if (serverProcess.killed || !serverProcess.pid) {
|
|
62
|
+
console.log(chalk.yellow('\n ⚠ Could not start WebSocket server. Make sure port 3001 is available.\n'));
|
|
63
|
+
} else {
|
|
64
|
+
console.log(chalk.green(' ✓ WebSocket server started on port 3001'));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
console.log('');
|
|
42
69
|
console.log(chalk.gray(' ─────────────────────────────────────────────────────'));
|
|
43
70
|
console.log(chalk.hex('#a855f7').bold(' Vynthen V Code — Local Bridge'));
|
|
@@ -45,18 +72,18 @@ export async function start(opts = {}) {
|
|
|
45
72
|
console.log('');
|
|
46
73
|
console.log(chalk.white(' User: ') + chalk.cyan(user || 'authenticated'));
|
|
47
74
|
console.log(chalk.white(' Mode: ') + chalk.gray(opts.approveAll ? 'Auto-approve' : 'Human-in-the-loop'));
|
|
48
|
-
console.log(chalk.white(' Endpoint: ') + chalk.gray(
|
|
75
|
+
console.log(chalk.white(' Endpoint: ') + chalk.gray(serverUrl));
|
|
49
76
|
console.log('');
|
|
50
77
|
|
|
51
|
-
|
|
78
|
+
let currentWorkingDir = process.cwd();
|
|
79
|
+
let terminalOutput = [];
|
|
80
|
+
|
|
52
81
|
const bridge = new Bridge({
|
|
53
82
|
onStatusChange: (status) => {
|
|
54
83
|
switch (status) {
|
|
55
84
|
case 'connected':
|
|
56
|
-
// Already handled inside bridge
|
|
57
85
|
break;
|
|
58
86
|
case 'reconnecting':
|
|
59
|
-
// Already handled inside bridge
|
|
60
87
|
break;
|
|
61
88
|
case 'auth_failed':
|
|
62
89
|
case 'token_expired':
|
|
@@ -67,17 +94,28 @@ export async function start(opts = {}) {
|
|
|
67
94
|
}
|
|
68
95
|
},
|
|
69
96
|
onOperation: (opType, params) => {
|
|
70
|
-
// Logged inside bridge
|
|
71
97
|
},
|
|
72
98
|
onError: (err) => {
|
|
73
|
-
|
|
99
|
+
},
|
|
100
|
+
onWorkingDirChange: (path) => {
|
|
101
|
+
currentWorkingDir = path;
|
|
102
|
+
console.log(chalk.gray(` 📁 Working directory: ${path}`));
|
|
103
|
+
},
|
|
104
|
+
onTerminalOutput: (output) => {
|
|
105
|
+
terminalOutput.push(output);
|
|
106
|
+
if (terminalOutput.length > 100) {
|
|
107
|
+
terminalOutput = terminalOutput.slice(-100);
|
|
108
|
+
}
|
|
109
|
+
process.stdout.write(output);
|
|
74
110
|
},
|
|
75
111
|
});
|
|
76
112
|
|
|
77
|
-
// ── Graceful shutdown ──
|
|
78
113
|
const shutdown = () => {
|
|
79
114
|
console.log(chalk.gray('\n\n Shutting down V Code bridge...'));
|
|
80
115
|
bridge.disconnect();
|
|
116
|
+
if (serverProcess) {
|
|
117
|
+
serverProcess.kill();
|
|
118
|
+
}
|
|
81
119
|
console.log(chalk.gray(` Session duration: ${bridge.getUptime()}`));
|
|
82
120
|
console.log(chalk.gray(` Operations handled: ${bridge.operationCount}`));
|
|
83
121
|
console.log(chalk.green(' Goodbye! 👋\n'));
|
|
@@ -87,14 +125,15 @@ export async function start(opts = {}) {
|
|
|
87
125
|
process.on('SIGINT', shutdown);
|
|
88
126
|
process.on('SIGTERM', shutdown);
|
|
89
127
|
|
|
90
|
-
// ── Connect ──
|
|
91
128
|
try {
|
|
92
129
|
await bridge.connect();
|
|
93
130
|
} catch (err) {
|
|
94
131
|
console.log(chalk.red(` ✗ ${err.message}\n`));
|
|
132
|
+
if (serverProcess) {
|
|
133
|
+
serverProcess.kill();
|
|
134
|
+
}
|
|
95
135
|
process.exit(1);
|
|
96
136
|
}
|
|
97
137
|
|
|
98
|
-
// Keep process alive
|
|
99
138
|
await new Promise(() => {});
|
|
100
|
-
}
|
|
139
|
+
}
|
package/lib/server.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V Code WebSocket Server
|
|
3
|
+
* Handles operations from the Vynthen V Code frontend
|
|
4
|
+
* Run with: node lib/server.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { WebSocketServer } from 'ws';
|
|
8
|
+
import * as http from 'http';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
|
|
14
|
+
const server = http.createServer((req, res) => {
|
|
15
|
+
if (req.url === '/health') {
|
|
16
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
17
|
+
res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() }));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
res.writeHead(200);
|
|
21
|
+
res.end('Vynthen V Code WS Bridge');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const wss = new WebSocketServer({ server });
|
|
25
|
+
|
|
26
|
+
const connectedClients = new Map();
|
|
27
|
+
let currentWorkingDir = process.cwd();
|
|
28
|
+
|
|
29
|
+
function log(msg) {
|
|
30
|
+
console.log(`[WS] ${new Date().toISOString()} - ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getProjectPath(params) {
|
|
34
|
+
const basePath = params.projectPath || currentWorkingDir;
|
|
35
|
+
return path.resolve(basePath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function executeOperation(operation, params, ws) {
|
|
39
|
+
try {
|
|
40
|
+
const projectPath = getProjectPath(params);
|
|
41
|
+
|
|
42
|
+
switch (operation) {
|
|
43
|
+
case 'read_file': {
|
|
44
|
+
const fullPath = path.join(projectPath, params.path);
|
|
45
|
+
if (!fullPath.startsWith(projectPath)) {
|
|
46
|
+
return { success: false, error: 'Path traversal not allowed' };
|
|
47
|
+
}
|
|
48
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
49
|
+
return { success: true, content };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case 'write_file': {
|
|
53
|
+
const fullPath = path.join(projectPath, params.path);
|
|
54
|
+
if (!fullPath.startsWith(projectPath)) {
|
|
55
|
+
return { success: false, error: 'Path traversal not allowed' };
|
|
56
|
+
}
|
|
57
|
+
const dir = path.dirname(fullPath);
|
|
58
|
+
if (!fs.existsSync(dir)) {
|
|
59
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
fs.writeFileSync(fullPath, params.content, 'utf8');
|
|
62
|
+
return { success: true, path: params.path };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case 'create_dir': {
|
|
66
|
+
const fullPath = path.join(projectPath, params.path);
|
|
67
|
+
if (!fullPath.startsWith(projectPath)) {
|
|
68
|
+
return { success: false, error: 'Path traversal not allowed' };
|
|
69
|
+
}
|
|
70
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
71
|
+
return { success: true, path: params.path };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
case 'delete_path': {
|
|
75
|
+
const fullPath = path.join(projectPath, params.path);
|
|
76
|
+
if (!fullPath.startsWith(projectPath)) {
|
|
77
|
+
return { success: false, error: 'Path traversal not allowed' };
|
|
78
|
+
}
|
|
79
|
+
if (fs.existsSync(fullPath)) {
|
|
80
|
+
const stat = fs.statSync(fullPath);
|
|
81
|
+
if (stat.isDirectory()) {
|
|
82
|
+
fs.rmSync(fullPath, { recursive: true });
|
|
83
|
+
} else {
|
|
84
|
+
fs.unlinkSync(fullPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { success: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'list_dir': {
|
|
91
|
+
const fullPath = path.join(projectPath, params.path || '.');
|
|
92
|
+
if (!fullPath.startsWith(projectPath)) {
|
|
93
|
+
return { success: false, error: 'Path traversal not allowed' };
|
|
94
|
+
}
|
|
95
|
+
if (!fs.existsSync(fullPath)) {
|
|
96
|
+
return { success: false, error: 'Directory does not exist' };
|
|
97
|
+
}
|
|
98
|
+
const items = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
99
|
+
const result = items.map(item => ({
|
|
100
|
+
name: item.name,
|
|
101
|
+
path: path.join(params.path || '.', item.name),
|
|
102
|
+
isDirectory: item.isDirectory(),
|
|
103
|
+
isFile: item.isFile()
|
|
104
|
+
}));
|
|
105
|
+
return { success: true, items: result, path: params.path || '.' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'run_command': {
|
|
109
|
+
const cwd = path.join(projectPath, params.cwd || '.');
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
const child = spawn(params.command, params.args || [], {
|
|
112
|
+
shell: true,
|
|
113
|
+
cwd: cwd
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
let stdout = '';
|
|
117
|
+
let stderr = '';
|
|
118
|
+
|
|
119
|
+
child.stdout.on('data', (data) => {
|
|
120
|
+
const str = data.toString();
|
|
121
|
+
stdout += str;
|
|
122
|
+
ws.send(JSON.stringify({ type: 'TERMINAL_OUTPUT', data: str }));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
child.stderr.on('data', (data) => {
|
|
126
|
+
const str = data.toString();
|
|
127
|
+
stderr += str;
|
|
128
|
+
ws.send(JSON.stringify({ type: 'TERMINAL_OUTPUT', data: str, isError: true }));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
child.on('close', (code) => {
|
|
132
|
+
resolve({ success: code === 0, stdout, stderr, exitCode: code });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
child.on('error', (err) => {
|
|
136
|
+
resolve({ success: false, error: err.message });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
child.kill();
|
|
141
|
+
resolve({ success: false, error: 'Command timeout', stdout, stderr });
|
|
142
|
+
}, params.timeout || 60000);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case 'set_working_directory': {
|
|
147
|
+
const fullPath = path.resolve(projectPath, params.path);
|
|
148
|
+
if (fs.existsSync(fullPath)) {
|
|
149
|
+
currentWorkingDir = fullPath;
|
|
150
|
+
return { success: true, path: fullPath };
|
|
151
|
+
}
|
|
152
|
+
return { success: false, error: 'Directory does not exist' };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case 'get_working_directory': {
|
|
156
|
+
return { success: true, path: currentWorkingDir };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case 'get_platform_info': {
|
|
160
|
+
return {
|
|
161
|
+
success: true,
|
|
162
|
+
platform: process.platform,
|
|
163
|
+
arch: process.arch,
|
|
164
|
+
homedir: os.homedir(),
|
|
165
|
+
hostname: os.hostname(),
|
|
166
|
+
username: os.userInfo().username,
|
|
167
|
+
cwd: currentWorkingDir
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'get_file_tree': {
|
|
172
|
+
const fullPath = path.join(projectPath, params.path || '.');
|
|
173
|
+
if (!fullPath.startsWith(projectPath)) {
|
|
174
|
+
return { success: false, error: 'Path traversal not allowed' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function buildTree(dirPath, depth = 0) {
|
|
178
|
+
if (depth > 5) return null;
|
|
179
|
+
try {
|
|
180
|
+
const items = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
181
|
+
const tree = [];
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
const fullItemPath = path.join(dirPath, item.name);
|
|
184
|
+
const relativePath = path.relative(projectPath, fullItemPath);
|
|
185
|
+
|
|
186
|
+
if (item.name.startsWith('.') || item.name === 'node_modules' || item.name === 'dist' || item.name === 'build') {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const node = {
|
|
191
|
+
name: item.name,
|
|
192
|
+
path: relativePath,
|
|
193
|
+
isDirectory: item.isDirectory()
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (item.isDirectory()) {
|
|
197
|
+
node.children = buildTree(fullItemPath, depth + 1);
|
|
198
|
+
}
|
|
199
|
+
tree.push(node);
|
|
200
|
+
}
|
|
201
|
+
return tree.sort((a, b) => {
|
|
202
|
+
if (a.isDirectory === b.isDirectory) return a.name.localeCompare(b.name);
|
|
203
|
+
return a.isDirectory ? -1 : 1;
|
|
204
|
+
});
|
|
205
|
+
} catch (e) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const tree = buildTree(fullPath);
|
|
211
|
+
return { success: true, tree, path: params.path || '.' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case 'file_exists': {
|
|
215
|
+
const fullPath = path.join(projectPath, params.path);
|
|
216
|
+
return { success: true, exists: fs.existsSync(fullPath) };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case 'read_directory_tree': {
|
|
220
|
+
const fullPath = path.join(projectPath, params.path || '.');
|
|
221
|
+
if (!fs.existsSync(fullPath)) {
|
|
222
|
+
return { success: false, error: 'Directory does not exist' };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function scanDir(dirPath, maxDepth = 3, currentDepth = 0) {
|
|
226
|
+
if (currentDepth >= maxDepth) return [];
|
|
227
|
+
|
|
228
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
229
|
+
const result = []
|
|
230
|
+
|
|
231
|
+
for (const entry of entries) {
|
|
232
|
+
if (entry.name.startsWith('.') || ['node_modules', 'dist', 'build', '.git', '__pycache__'].includes(entry.name)) {
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const fullEntryPath = path.join(dirPath, entry.name)
|
|
237
|
+
const relativePath = path.relative(projectPath, fullEntryPath)
|
|
238
|
+
|
|
239
|
+
result.push({
|
|
240
|
+
name: entry.name,
|
|
241
|
+
path: relativePath,
|
|
242
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
243
|
+
...(entry.isDirectory() && currentDepth < maxDepth - 1 ? {
|
|
244
|
+
children: scanDir(fullEntryPath, maxDepth, currentDepth + 1)
|
|
245
|
+
} : {})
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return result.sort((a, b) => {
|
|
250
|
+
if (a.type === b.type) return a.name.localeCompare(b.name)
|
|
251
|
+
return a.type === 'directory' ? -1 : 1
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { success: true, entries: scanDir(fullPath), path: params.path || '.' };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
default:
|
|
259
|
+
return { success: false, error: `Unknown operation: ${operation}` };
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return { success: false, error: error.message };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
wss.on('connection', (ws, req) => {
|
|
267
|
+
const clientId = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
268
|
+
log(`Client connected: ${clientId} from ${req.socket.remoteAddress}`);
|
|
269
|
+
|
|
270
|
+
connectedClients.set(ws, { id: clientId, connectedAt: Date.now(), isCLI: false, platform: null });
|
|
271
|
+
|
|
272
|
+
ws.isAuthenticated = false;
|
|
273
|
+
|
|
274
|
+
ws.on('message', async (message) => {
|
|
275
|
+
try {
|
|
276
|
+
const parsed = JSON.parse(message.toString());
|
|
277
|
+
log(`Received: ${parsed.type}`);
|
|
278
|
+
|
|
279
|
+
if (parsed.type === 'CLIENT_INFO') {
|
|
280
|
+
ws.clientData = parsed.data;
|
|
281
|
+
ws.isAuthenticated = true;
|
|
282
|
+
|
|
283
|
+
connectedClients.set(ws, {
|
|
284
|
+
id: clientId,
|
|
285
|
+
connectedAt: Date.now(),
|
|
286
|
+
isCLI: true,
|
|
287
|
+
platform: parsed.data.platform
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
currentWorkingDir = parsed.data.cwd || process.cwd();
|
|
291
|
+
|
|
292
|
+
ws.send(JSON.stringify({
|
|
293
|
+
type: 'SERVER_MESSAGE',
|
|
294
|
+
data: {
|
|
295
|
+
message: 'Successfully connected to Vynthen V Code backend.',
|
|
296
|
+
workingDir: currentWorkingDir
|
|
297
|
+
}
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
log(`CLI authenticated: ${parsed.data.username || 'unknown'} on ${parsed.data.platform} at ${currentWorkingDir}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
else if (parsed.type === 'REGISTER_FRONTEND') {
|
|
304
|
+
connectedClients.set(ws, {
|
|
305
|
+
id: clientId,
|
|
306
|
+
connectedAt: Date.now(),
|
|
307
|
+
isCLI: false,
|
|
308
|
+
platform: null
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
ws.send(JSON.stringify({
|
|
312
|
+
type: 'WORKING_DIR',
|
|
313
|
+
data: { path: currentWorkingDir }
|
|
314
|
+
}));
|
|
315
|
+
|
|
316
|
+
log('Frontend registered');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
else if (parsed.type === 'OPERATION' || parsed.type === 'OPERATION_REQUEST') {
|
|
320
|
+
const operation = parsed.data?.operation || parsed.operation;
|
|
321
|
+
const params = parsed.data?.params || parsed.params || {};
|
|
322
|
+
const id = parsed.id || Date.now().toString();
|
|
323
|
+
|
|
324
|
+
log(`Operation: ${operation} on ${params.path || 'root'}`);
|
|
325
|
+
|
|
326
|
+
const result = await executeOperation(operation, params, ws);
|
|
327
|
+
|
|
328
|
+
connectedClients.forEach((info, clientWs) => {
|
|
329
|
+
if (!info.isCLI && clientWs.readyState === clientWs.OPEN) {
|
|
330
|
+
clientWs.send(JSON.stringify({
|
|
331
|
+
type: 'OPERATION_COMPLETED',
|
|
332
|
+
data: { operation, params, result }
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
ws.send(JSON.stringify({
|
|
338
|
+
type: 'OPERATION_RESULT',
|
|
339
|
+
id,
|
|
340
|
+
data: result
|
|
341
|
+
}));
|
|
342
|
+
|
|
343
|
+
if (result.success) {
|
|
344
|
+
log(`Operation completed: ${operation}`);
|
|
345
|
+
} else {
|
|
346
|
+
log(`Operation failed: ${operation} - ${result.error}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
else if (parsed.type === 'PING') {
|
|
351
|
+
ws.send(JSON.stringify({ type: 'PONG' }));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
else if (parsed.type === 'SET_WORKING_DIR') {
|
|
355
|
+
const newPath = path.resolve(currentWorkingDir, parsed.path);
|
|
356
|
+
if (fs.existsSync(newPath) && fs.statSync(newPath).isDirectory()) {
|
|
357
|
+
currentWorkingDir = newPath;
|
|
358
|
+
ws.send(JSON.stringify({
|
|
359
|
+
type: 'WORKING_DIR',
|
|
360
|
+
data: { path: currentWorkingDir }
|
|
361
|
+
}));
|
|
362
|
+
|
|
363
|
+
connectedClients.forEach((info, clientWs) => {
|
|
364
|
+
if (clientWs.readyState === clientWs.OPEN) {
|
|
365
|
+
clientWs.send(JSON.stringify({
|
|
366
|
+
type: 'WORKING_DIR',
|
|
367
|
+
data: { path: currentWorkingDir }
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
log(`Working directory changed to: ${currentWorkingDir}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
} catch (e) {
|
|
377
|
+
log(`Parse error: ${e.message}`);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
ws.on('close', () => {
|
|
382
|
+
const info = connectedClients.get(ws);
|
|
383
|
+
log(`Client disconnected: ${info?.id || 'unknown'} (CLI: ${info?.isCLI})`);
|
|
384
|
+
connectedClients.delete(ws);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
ws.on('error', (err) => {
|
|
388
|
+
log(`WebSocket error: ${err.message}`);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
ws.send(JSON.stringify({
|
|
392
|
+
type: 'SERVER_MESSAGE',
|
|
393
|
+
data: { message: 'Connected to Vynthen V Code Bridge. Waiting for authentication...' }
|
|
394
|
+
}));
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
setInterval(() => {
|
|
398
|
+
connectedClients.forEach((info, ws) => {
|
|
399
|
+
if (ws.readyState === ws.OPEN) {
|
|
400
|
+
ws.send(JSON.stringify({ type: 'PING' }));
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}, 30000);
|
|
404
|
+
|
|
405
|
+
const PORT = process.env.VCODE_WS_PORT || 3001;
|
|
406
|
+
server.listen(PORT, () => {
|
|
407
|
+
log(`V Code WebSocket server running on port ${PORT}`);
|
|
408
|
+
log(`Working directory: ${currentWorkingDir}`);
|
|
409
|
+
log(`V Code is ready! Connect your Vynthen V Code frontend to ws://localhost:${PORT}`);
|
|
410
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vcode-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Connect your local machine to Vynthen V Code — a powerful AI code agent with full system control, secure keychain auth, and human-in-the-loop permissions.",
|
|
6
6
|
"main": "bin/vcode.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node bin/vcode.js start",
|
|
12
|
+
"serve": "node lib/server.js",
|
|
12
13
|
"test": "echo \"No tests yet\" && exit 0"
|
|
13
14
|
},
|
|
14
15
|
"keywords": [
|
|
@@ -52,4 +53,4 @@
|
|
|
52
53
|
"README.md",
|
|
53
54
|
"LICENSE"
|
|
54
55
|
]
|
|
55
|
-
}
|
|
56
|
+
}
|