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 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 = 'wss://vynthen.com/api/vcode-ws';
14
- const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 15000, 30000]; // exponential backoff
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: process.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
+ }
@@ -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('wss://vynthen.com/api/vcode-ws'));
75
+ console.log(chalk.white(' Endpoint: ') + chalk.gray(serverUrl));
49
76
  console.log('');
50
77
 
51
- // ── Launch bridge ──
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
- // Logged inside bridge
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.1",
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
+ }