vigthoria-cli 1.6.53 → 1.6.54

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.
@@ -47,6 +47,7 @@ const api_js_1 = require("../utils/api.js");
47
47
  const tools_js_1 = require("../utils/tools.js");
48
48
  const session_js_1 = require("../utils/session.js");
49
49
  const bridge_client_js_1 = require("../utils/bridge-client.js");
50
+ const workspace_stream_js_1 = require("../utils/workspace-stream.js");
50
51
  const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
51
52
  const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS || '3900000';
52
53
  const parsed = Number.parseInt(rawValue, 10);
@@ -1442,6 +1443,17 @@ class ChatCommand {
1442
1443
  targetPath: this.currentProjectPath,
1443
1444
  ...runtimeContext,
1444
1445
  };
1446
+ // Start workspace watcher for bidirectional real-time sync
1447
+ let watcher = null;
1448
+ if (this.currentProjectPath && fs.existsSync(this.currentProjectPath)) {
1449
+ watcher = new workspace_stream_js_1.WorkspaceWatcher({
1450
+ workspaceRoot: this.currentProjectPath,
1451
+ onFileChange: (relativePath, content, action) => {
1452
+ this.logger.debug(`Local change detected: ${action} ${relativePath}`);
1453
+ },
1454
+ });
1455
+ watcher.start();
1456
+ }
1445
1457
  try {
1446
1458
  const workflowPromise = this.api.runV3AgentWorkflow(executionPrompt, {
1447
1459
  workspace: { path: this.currentProjectPath },
@@ -1484,6 +1496,7 @@ class ChatCommand {
1484
1496
  }
1485
1497
  this.logger.warn('Falling back to legacy CLI agent loop');
1486
1498
  this.logger.debug(`V3 agent workflow returned an incomplete result: ${previewGate?.error || 'workspace changes were not fully validated'}`);
1499
+ watcher?.stop();
1487
1500
  return false;
1488
1501
  }
1489
1502
  const errorMessage = `V3 agent workflow returned an incomplete result and legacy fallback is disabled. ${previewGate?.error || 'Workspace changes were not fully validated.'}`;
@@ -1504,6 +1517,7 @@ class ChatCommand {
1504
1517
  metadata: { executionPath: 'v3-agent', previewGate },
1505
1518
  }, null, 2));
1506
1519
  }
1520
+ watcher?.stop();
1507
1521
  return true;
1508
1522
  }
1509
1523
  if (!this.jsonOutput && previewGate?.required && previewGate?.passed !== true && workspaceHasOutput) {
@@ -1543,9 +1557,11 @@ class ChatCommand {
1543
1557
  }
1544
1558
  }
1545
1559
  this.messages.push({ role: 'assistant', content: response.content || 'V3 agent workflow completed.' });
1560
+ watcher?.stop();
1546
1561
  return true;
1547
1562
  }
1548
1563
  catch (error) {
1564
+ watcher?.stop();
1549
1565
  if (rescueEligible && !this.api.hasAgentWorkspaceOutput(workspaceContext)) {
1550
1566
  const rescued = await this.tryCommandLevelSaaSRescue(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
1551
1567
  if (rescued) {
package/dist/utils/api.js CHANGED
@@ -17,6 +17,7 @@ const https_1 = __importDefault(require("https"));
17
17
  const net_1 = __importDefault(require("net"));
18
18
  const path_1 = __importDefault(require("path"));
19
19
  const ws_1 = __importDefault(require("ws"));
20
+ const workspace_stream_js_1 = require("./workspace-stream.js");
20
21
  class CLIError extends Error {
21
22
  category;
22
23
  statusCode;
@@ -2636,6 +2637,19 @@ document.addEventListener('DOMContentLoaded', () => {
2636
2637
  serverWorkspaceRoot = event.workspace_root.trim();
2637
2638
  }
2638
2639
  this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
2640
+ // Real-time workspace streaming: apply file mutations to local disk immediately
2641
+ if (event.type === 'file_mutation') {
2642
+ const localRoot = context.projectPath || context.workspacePath || context.targetPath;
2643
+ if (localRoot && typeof event.path === 'string') {
2644
+ (0, workspace_stream_js_1.applyFileMutation)(event, localRoot);
2645
+ if (typeof event.content === 'string') {
2646
+ const relPath = this.normalizeAgentWorkspaceRelativePath(event.path, serverWorkspaceRoot || undefined);
2647
+ if (relPath) {
2648
+ streamedFiles[relPath] = event.content;
2649
+ }
2650
+ }
2651
+ }
2652
+ }
2639
2653
  // Empty workspace guard: if the remote agent lists its root
2640
2654
  // and finds nothing while our local workspace has files, the
2641
2655
  // workspace was not hydrated. Abort early with a clear error
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Workspace Stream — Real-time bidirectional file sync between CLI and V3.
3
+ *
4
+ * Architecture:
5
+ * 1. WorkspaceWatcher — chokidar-based file watcher that detects local changes
6
+ * and pushes them to V3 via WebSocket.
7
+ * 2. MutationApplier — receives file_mutation events from V3's SSE stream
8
+ * and applies them to the local workspace IN REAL TIME (not at completion).
9
+ * 3. WorkspaceWSClient — WebSocket client connected to V3's /ws/workspace
10
+ * for bidirectional file sync outside the SSE stream.
11
+ *
12
+ * Event types (V3 → CLI via SSE):
13
+ * {"type": "file_mutation", "path": "rel/path", "content": "...", "action": "write"|"edit", "tool": "write_file"|"edit_file"}
14
+ *
15
+ * Event types (CLI → V3 via /ws/workspace):
16
+ * {"type": "bind", "context_id": "...", "workspace_root": "..."}
17
+ * {"type": "file_sync", "path": "rel/path", "content": "...", "action": "write"|"delete"}
18
+ * {"type": "file_batch", "files": [{path, content}, ...]}
19
+ */
20
+ /**
21
+ * Apply a file_mutation event from V3's SSE stream to the local workspace.
22
+ * Call this from the SSE event handler for real-time file application.
23
+ */
24
+ export declare function applyFileMutation(event: {
25
+ type: string;
26
+ path: string;
27
+ content: string;
28
+ action: string;
29
+ }, workspaceRoot: string): boolean;
30
+ export interface WatcherOptions {
31
+ workspaceRoot: string;
32
+ onFileChange?: (relativePath: string, content: string | null, action: 'write' | 'delete') => void;
33
+ }
34
+ export declare class WorkspaceWatcher {
35
+ private watcher;
36
+ private workspaceRoot;
37
+ private onFileChange;
38
+ private _ready;
39
+ constructor(options: WatcherOptions);
40
+ start(): void;
41
+ stop(): void;
42
+ get isReady(): boolean;
43
+ private _handleChange;
44
+ }
45
+ export interface WorkspaceWSOptions {
46
+ serverUrl: string;
47
+ token: string;
48
+ contextId: string;
49
+ workspaceRoot: string;
50
+ onMutation?: (event: any) => void;
51
+ }
52
+ export declare class WorkspaceWSClient {
53
+ private ws;
54
+ private opts;
55
+ private reconnectTimer;
56
+ private _connected;
57
+ private _queue;
58
+ private _maxQueue;
59
+ constructor(opts: WorkspaceWSOptions);
60
+ connect(): void;
61
+ disconnect(): void;
62
+ /**
63
+ * Push a local file change to V3's workspace.
64
+ */
65
+ syncFile(relativePath: string, content: string | null, action: 'write' | 'delete'): void;
66
+ /**
67
+ * Push multiple files at once.
68
+ */
69
+ syncBatch(files: Array<{
70
+ path: string;
71
+ content: string;
72
+ }>): void;
73
+ get isConnected(): boolean;
74
+ private _send;
75
+ }
@@ -0,0 +1,301 @@
1
+ "use strict";
2
+ /**
3
+ * Workspace Stream — Real-time bidirectional file sync between CLI and V3.
4
+ *
5
+ * Architecture:
6
+ * 1. WorkspaceWatcher — chokidar-based file watcher that detects local changes
7
+ * and pushes them to V3 via WebSocket.
8
+ * 2. MutationApplier — receives file_mutation events from V3's SSE stream
9
+ * and applies them to the local workspace IN REAL TIME (not at completion).
10
+ * 3. WorkspaceWSClient — WebSocket client connected to V3's /ws/workspace
11
+ * for bidirectional file sync outside the SSE stream.
12
+ *
13
+ * Event types (V3 → CLI via SSE):
14
+ * {"type": "file_mutation", "path": "rel/path", "content": "...", "action": "write"|"edit", "tool": "write_file"|"edit_file"}
15
+ *
16
+ * Event types (CLI → V3 via /ws/workspace):
17
+ * {"type": "bind", "context_id": "...", "workspace_root": "..."}
18
+ * {"type": "file_sync", "path": "rel/path", "content": "...", "action": "write"|"delete"}
19
+ * {"type": "file_batch", "files": [{path, content}, ...]}
20
+ */
21
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ var desc = Object.getOwnPropertyDescriptor(m, k);
24
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
25
+ desc = { enumerable: true, get: function() { return m[k]; } };
26
+ }
27
+ Object.defineProperty(o, k2, desc);
28
+ }) : (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ o[k2] = m[k];
31
+ }));
32
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
33
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
34
+ }) : function(o, v) {
35
+ o["default"] = v;
36
+ });
37
+ var __importStar = (this && this.__importStar) || (function () {
38
+ var ownKeys = function(o) {
39
+ ownKeys = Object.getOwnPropertyNames || function (o) {
40
+ var ar = [];
41
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
42
+ return ar;
43
+ };
44
+ return ownKeys(o);
45
+ };
46
+ return function (mod) {
47
+ if (mod && mod.__esModule) return mod;
48
+ var result = {};
49
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
50
+ __setModuleDefault(result, mod);
51
+ return result;
52
+ };
53
+ })();
54
+ var __importDefault = (this && this.__importDefault) || function (mod) {
55
+ return (mod && mod.__esModule) ? mod : { "default": mod };
56
+ };
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.WorkspaceWSClient = exports.WorkspaceWatcher = void 0;
59
+ exports.applyFileMutation = applyFileMutation;
60
+ const chokidar = __importStar(require("chokidar"));
61
+ const fs_1 = __importDefault(require("fs"));
62
+ const path_1 = __importDefault(require("path"));
63
+ const ws_1 = __importDefault(require("ws"));
64
+ const logger_js_1 = require("./logger.js");
65
+ const logger = new logger_js_1.Logger();
66
+ logger.setVerbose(!!process.env.VIGTHORIA_DEBUG);
67
+ // Files/dirs to ignore in the watcher
68
+ const IGNORE_PATTERNS = [
69
+ '**/node_modules/**',
70
+ '**/.git/**',
71
+ '**/__pycache__/**',
72
+ '**/.venv/**',
73
+ '**/dist/**',
74
+ '**/build/**',
75
+ '**/.next/**',
76
+ '**/.cache/**',
77
+ '**/.vigthoria/**',
78
+ '**/coverage/**',
79
+ '**/*.pyc',
80
+ ];
81
+ const BINARY_EXTENSIONS = new Set([
82
+ '.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.webp', '.bmp',
83
+ '.mp3', '.mp4', '.wav', '.ogg', '.webm', '.avi',
84
+ '.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
85
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
86
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx',
87
+ '.pyc', '.pyo', '.so', '.dll', '.exe', '.bin',
88
+ '.db', '.sqlite', '.sqlite3',
89
+ ]);
90
+ const MAX_SYNC_FILE_BYTES = 500 * 1024; // 500 KB
91
+ // ── Mutation Applier ─────────────────────────────────────────
92
+ /**
93
+ * Tracks files that V3 has mutated so the watcher doesn't echo them back.
94
+ */
95
+ const _v3MutatedFiles = new Set();
96
+ const _v3MuteTimeout = 2000; // 2s mute window after V3 writes a file
97
+ /**
98
+ * Apply a file_mutation event from V3's SSE stream to the local workspace.
99
+ * Call this from the SSE event handler for real-time file application.
100
+ */
101
+ function applyFileMutation(event, workspaceRoot) {
102
+ if (event.type !== 'file_mutation')
103
+ return false;
104
+ if (!event.path || !workspaceRoot)
105
+ return false;
106
+ const absPath = path_1.default.resolve(workspaceRoot, event.path);
107
+ // Safety: ensure the resolved path is within the workspace
108
+ if (!absPath.startsWith(path_1.default.resolve(workspaceRoot) + path_1.default.sep) && absPath !== path_1.default.resolve(workspaceRoot)) {
109
+ logger.warn(`Refusing to apply mutation outside workspace: ${event.path}`);
110
+ return false;
111
+ }
112
+ try {
113
+ if (event.action === 'delete') {
114
+ if (fs_1.default.existsSync(absPath)) {
115
+ fs_1.default.unlinkSync(absPath);
116
+ logger.debug(`Deleted: ${event.path}`);
117
+ }
118
+ }
119
+ else if (typeof event.content === 'string') {
120
+ fs_1.default.mkdirSync(path_1.default.dirname(absPath), { recursive: true });
121
+ fs_1.default.writeFileSync(absPath, event.content, 'utf8');
122
+ // Mute the watcher for this file to prevent echo
123
+ _v3MutatedFiles.add(absPath);
124
+ setTimeout(() => _v3MutatedFiles.delete(absPath), _v3MuteTimeout);
125
+ logger.debug(`Applied: ${event.path} (${event.action})`);
126
+ }
127
+ return true;
128
+ }
129
+ catch (err) {
130
+ logger.error(`Failed to apply mutation for ${event.path}: ${err}`);
131
+ return false;
132
+ }
133
+ }
134
+ class WorkspaceWatcher {
135
+ watcher = null;
136
+ workspaceRoot;
137
+ onFileChange;
138
+ _ready = false;
139
+ constructor(options) {
140
+ this.workspaceRoot = path_1.default.resolve(options.workspaceRoot);
141
+ this.onFileChange = options.onFileChange;
142
+ }
143
+ start() {
144
+ if (this.watcher)
145
+ return;
146
+ this.watcher = chokidar.watch(this.workspaceRoot, {
147
+ ignored: IGNORE_PATTERNS,
148
+ persistent: true,
149
+ ignoreInitial: true,
150
+ awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 },
151
+ });
152
+ this.watcher
153
+ .on('ready', () => {
154
+ this._ready = true;
155
+ logger.debug('Workspace watcher ready');
156
+ })
157
+ .on('add', (filePath) => this._handleChange(filePath, 'write'))
158
+ .on('change', (filePath) => this._handleChange(filePath, 'write'))
159
+ .on('unlink', (filePath) => this._handleChange(filePath, 'delete'));
160
+ }
161
+ stop() {
162
+ if (this.watcher) {
163
+ this.watcher.close();
164
+ this.watcher = null;
165
+ this._ready = false;
166
+ }
167
+ }
168
+ get isReady() {
169
+ return this._ready;
170
+ }
171
+ _handleChange(filePath, action) {
172
+ // Skip if this file was just written by V3 (echo prevention)
173
+ if (_v3MutatedFiles.has(filePath))
174
+ return;
175
+ const ext = path_1.default.extname(filePath).toLowerCase();
176
+ if (BINARY_EXTENSIONS.has(ext))
177
+ return;
178
+ const relativePath = path_1.default.relative(this.workspaceRoot, filePath);
179
+ if (relativePath.startsWith('..'))
180
+ return; // safety
181
+ if (action === 'delete') {
182
+ this.onFileChange?.(relativePath, null, 'delete');
183
+ return;
184
+ }
185
+ try {
186
+ const stat = fs_1.default.statSync(filePath);
187
+ if (stat.size > MAX_SYNC_FILE_BYTES)
188
+ return;
189
+ const content = fs_1.default.readFileSync(filePath, 'utf8');
190
+ this.onFileChange?.(relativePath, content, 'write');
191
+ }
192
+ catch {
193
+ // File might have been deleted between event and read
194
+ }
195
+ }
196
+ }
197
+ exports.WorkspaceWatcher = WorkspaceWatcher;
198
+ class WorkspaceWSClient {
199
+ ws = null;
200
+ opts;
201
+ reconnectTimer = null;
202
+ _connected = false;
203
+ _queue = [];
204
+ _maxQueue = 200;
205
+ constructor(opts) {
206
+ this.opts = opts;
207
+ }
208
+ connect() {
209
+ if (this.ws)
210
+ return;
211
+ const url = `${this.opts.serverUrl}/ws/workspace?token=${encodeURIComponent(this.opts.token)}`;
212
+ this.ws = new ws_1.default(url);
213
+ this.ws.on('open', () => {
214
+ this._connected = true;
215
+ // Bind to workspace
216
+ this._send({
217
+ type: 'bind',
218
+ context_id: this.opts.contextId,
219
+ workspace_root: this.opts.workspaceRoot,
220
+ });
221
+ // Flush queued messages
222
+ while (this._queue.length > 0) {
223
+ const msg = this._queue.shift();
224
+ this._send(msg);
225
+ }
226
+ logger.debug('WS workspace connected');
227
+ });
228
+ this.ws.on('message', (data) => {
229
+ try {
230
+ const event = JSON.parse(data.toString());
231
+ if (event.type === 'file_mutation') {
232
+ this.opts.onMutation?.(event);
233
+ }
234
+ }
235
+ catch {
236
+ // ignore parse errors
237
+ }
238
+ });
239
+ this.ws.on('close', () => {
240
+ this._connected = false;
241
+ this.ws = null;
242
+ // Auto-reconnect after 3s
243
+ this.reconnectTimer = setTimeout(() => this.connect(), 3000);
244
+ });
245
+ this.ws.on('error', () => {
246
+ // error handler to prevent crash, close event handles reconnect
247
+ });
248
+ }
249
+ disconnect() {
250
+ if (this.reconnectTimer) {
251
+ clearTimeout(this.reconnectTimer);
252
+ this.reconnectTimer = null;
253
+ }
254
+ if (this.ws) {
255
+ this.ws.close();
256
+ this.ws = null;
257
+ }
258
+ this._connected = false;
259
+ }
260
+ /**
261
+ * Push a local file change to V3's workspace.
262
+ */
263
+ syncFile(relativePath, content, action) {
264
+ const msg = {
265
+ type: 'file_sync',
266
+ path: relativePath,
267
+ content: content || '',
268
+ action,
269
+ };
270
+ if (this._connected) {
271
+ this._send(msg);
272
+ }
273
+ else if (this._queue.length < this._maxQueue) {
274
+ this._queue.push(msg);
275
+ }
276
+ }
277
+ /**
278
+ * Push multiple files at once.
279
+ */
280
+ syncBatch(files) {
281
+ const msg = { type: 'file_batch', files };
282
+ if (this._connected) {
283
+ this._send(msg);
284
+ }
285
+ else if (this._queue.length < this._maxQueue) {
286
+ this._queue.push(msg);
287
+ }
288
+ }
289
+ get isConnected() {
290
+ return this._connected;
291
+ }
292
+ _send(msg) {
293
+ try {
294
+ this.ws?.send(JSON.stringify(msg));
295
+ }
296
+ catch {
297
+ // connection lost
298
+ }
299
+ }
300
+ }
301
+ exports.WorkspaceWSClient = WorkspaceWSClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.53",
3
+ "version": "1.6.54",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -61,6 +61,7 @@
61
61
  "archiver": "^6.0.1",
62
62
  "axios": "^1.6.0",
63
63
  "chalk": "^5.3.0",
64
+ "chokidar": "^5.0.0",
64
65
  "commander": "^11.1.0",
65
66
  "conf": "^12.0.0",
66
67
  "diff": "^5.1.0",
@@ -86,4 +87,4 @@
86
87
  "engines": {
87
88
  "node": ">=18.0.0"
88
89
  }
89
- }
90
+ }