vigthoria-cli 1.6.52 → 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.
package/README.md CHANGED
@@ -24,6 +24,11 @@ AI-powered terminal coding assistant integrated with Vigthoria's AI models and s
24
24
  - 🔐 **Secure Auth** - Integration with Vigthoria accounts
25
25
  - 📦 **Project Context** - Understands your codebase
26
26
  - 🗂️ **Vigthoria Repo** - Push/Pull projects to your personal cloud repository
27
+ - 🔍 **Deep Codebase Search** - Indexed search across files, symbols, and content via ripgrep
28
+ - ✏️ **Atomic Multi-File Edits** - Coordinated changes across multiple files with rollback
29
+ - 🧩 **Sub-Agent Delegation** - Spawn focused sub-agents for parallel investigation
30
+ - 🔒 **Persistent Permissions** - Per-project tool approvals saved across sessions
31
+ - 📝 **Multi-Line Input** - Block mode ({{{ }}}) and line continuation (\\) for complex prompts
27
32
 
28
33
  ## Installation
29
34
 
@@ -96,7 +101,7 @@ If you see `ENOTFOUND registry.npmjs.org`, try these solutions:
96
101
  4. **Direct tarball download:**
97
102
  ```bash
98
103
  # Download directly
99
- npm install -g https://cli.vigthoria.io/downloads/vigthoria-cli-1.6.12.tgz
104
+ npm install -g https://cli.vigthoria.io/downloads/vigthoria-cli-1.6.52.tgz
100
105
  ```
101
106
 
102
107
  5. **Use Git clone method (no npm registry needed):**
@@ -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) {
@@ -2473,13 +2489,16 @@ class ChatCommand {
2473
2489
  });
2474
2490
  console.log(action);
2475
2491
  const answer = await new Promise((resolve) => {
2476
- rl.question(chalk_1.default.yellow('Approve? [y]es / [n]o / [a]ll this turn: '), resolve);
2492
+ rl.question(chalk_1.default.yellow('Approve? [y]es / [n]o / [a]ll this turn / [p]ersist: '), resolve);
2477
2493
  });
2478
2494
  rl.close();
2479
2495
  const normalized = answer.trim().toLowerCase();
2480
2496
  if (normalized === 'a' || normalized === 'all') {
2481
2497
  return 'batch';
2482
2498
  }
2499
+ if (normalized === 'p' || normalized === 'persist') {
2500
+ return 'persist';
2501
+ }
2483
2502
  return normalized === 'y' || normalized === 'yes';
2484
2503
  }
2485
2504
  getCurrentSessionInfo() {
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
@@ -74,7 +74,7 @@ export declare class AgenticTools {
74
74
  private static permissionsFile;
75
75
  constructor(logger: Logger, cwd: string, permissionCallback: (action: string, options?: {
76
76
  batchApproval?: boolean;
77
- }) => Promise<boolean | 'batch'>, autoApprove?: boolean);
77
+ }) => Promise<boolean | 'batch' | 'persist'>, autoApprove?: boolean);
78
78
  /**
79
79
  * Load persistent permissions for the current project
80
80
  */
@@ -493,6 +493,11 @@ class AgenticTools {
493
493
  if (approved === 'batch') {
494
494
  this.sessionApprovedTools.add(normalizedCall.tool);
495
495
  }
496
+ else if (approved === 'persist') {
497
+ this.sessionApprovedTools.add(normalizedCall.tool);
498
+ this.savePersistentPermission(normalizedCall.tool);
499
+ this.logger.info(`${normalizedCall.tool}: Saved as persistent permission`);
500
+ }
496
501
  }
497
502
  }
498
503
  // Execute with retry logic for applicable operations
@@ -1069,6 +1074,11 @@ class AgenticTools {
1069
1074
  /\bmkfs\b/i, // Format filesystems
1070
1075
  />\s*\/dev\/sd/i, // Writing to disk devices
1071
1076
  /\bdd\s+.*of=/i, // dd write operations
1077
+ /\bnode\s+-e\b/i, // Node eval (sandbox bypass)
1078
+ /\bnode\s+--eval\b/i, // Node eval long form
1079
+ /\bpython3?\s+-c\b/i, // Python exec (sandbox bypass)
1080
+ /\bruby\s+-e\b/i, // Ruby eval (sandbox bypass)
1081
+ /\bperl\s+-e\b/i, // Perl eval (sandbox bypass)
1072
1082
  ];
1073
1083
  for (const pattern of blockedPatterns) {
1074
1084
  if (pattern.test(args.command)) {
@@ -1987,11 +1997,11 @@ class AgenticTools {
1987
1997
  // Build a focused system prompt for the sub-agent
1988
1998
  const systemPrompt = [
1989
1999
  'You are a focused sub-agent spawned to complete a specific subtask.',
1990
- 'You have access to all standard tools (read_file, write_file, edit_file, bash, grep, list_dir, glob, git).',
2000
+ 'You have access to standard tools (read_file, write_file, edit_file, bash, grep, list_dir, glob, git). You cannot spawn sub-agents.',
1991
2001
  'Complete the task thoroughly and return a detailed result.',
1992
2002
  `Working directory: ${workingDir}`,
1993
2003
  '',
1994
- AgenticTools.getToolsForPrompt(),
2004
+ AgenticTools.getToolsForPrompt().replace(/### task[\s\S]*?(?=###|$)/, ''), // Strip task from sub-agent
1995
2005
  ].join('\n');
1996
2006
  const messages = [
1997
2007
  { role: 'system', content: systemPrompt },
@@ -2059,6 +2069,7 @@ class AgenticTools {
2059
2069
  return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid edits JSON: ${parseError.message}`, 'Provide edits as a JSON array: [{"path": "file.ts", "old_text": "find", "new_text": "replace"}]');
2060
2070
  }
2061
2071
  // Validate all edits can proceed before modifying anything
2072
+ const contentMap = new Map();
2062
2073
  const backups = [];
2063
2074
  const resolvedEdits = [];
2064
2075
  for (let i = 0; i < edits.length; i++) {
@@ -2073,7 +2084,13 @@ class AgenticTools {
2073
2084
  if (!fs.existsSync(resolvedPath)) {
2074
2085
  return this.createErrorResult(ToolErrorType.FILE_NOT_FOUND, `Edit ${i}: file not found: ${edit.path}`, 'Use write_file to create new files instead.');
2075
2086
  }
2076
- const content = fs.readFileSync(resolvedPath, 'utf-8');
2087
+ // Use contentMap to track cumulative edits to the same file
2088
+ if (!contentMap.has(resolvedPath)) {
2089
+ const diskContent = fs.readFileSync(resolvedPath, 'utf-8');
2090
+ contentMap.set(resolvedPath, diskContent);
2091
+ backups.push({ path: resolvedPath, content: diskContent });
2092
+ }
2093
+ const content = contentMap.get(resolvedPath);
2077
2094
  if (!content.includes(edit.old_text)) {
2078
2095
  return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, `Edit ${i}: old_text not found in ${edit.path}`, `The text to replace was not found. Use read_file to verify the file contents.`);
2079
2096
  }
@@ -2082,15 +2099,16 @@ class AgenticTools {
2082
2099
  if (matchCount > 1) {
2083
2100
  return this.createErrorResult(ToolErrorType.EXECUTION_FAILED, `Edit ${i}: old_text matches ${matchCount} locations in ${edit.path}`, 'Make old_text more specific to match exactly one location. Include surrounding context.');
2084
2101
  }
2085
- backups.push({ path: resolvedPath, content });
2102
+ // Apply edit to contentMap so subsequent edits to same file see updated content
2103
+ contentMap.set(resolvedPath, content.replace(edit.old_text, edit.new_text));
2086
2104
  resolvedEdits.push({ resolvedPath, old_text: edit.old_text, new_text: edit.new_text, content });
2087
2105
  }
2088
2106
  // Apply all edits
2089
2107
  const applied = [];
2090
2108
  try {
2091
2109
  for (const edit of resolvedEdits) {
2092
- const newContent = edit.content.replace(edit.old_text, edit.new_text);
2093
- fs.writeFileSync(edit.resolvedPath, newContent, 'utf-8');
2110
+ const finalContent = contentMap.get(edit.resolvedPath) || edit.content.replace(edit.old_text, edit.new_text);
2111
+ fs.writeFileSync(edit.resolvedPath, finalContent, 'utf-8');
2094
2112
  applied.push(path.relative(this.cwd, edit.resolvedPath));
2095
2113
  }
2096
2114
  // Push undo operations for all edits
@@ -2163,7 +2181,7 @@ class AgenticTools {
2163
2181
  if (pattern) {
2164
2182
  const basename = entry.name;
2165
2183
  // Simple glob matching for extension patterns like *.ts, *.js
2166
- const globPattern = pattern.replace(/\*\*/g, '').replace(/\*/g, '.*').replace(/\?/g, '.');
2184
+ const globPattern = pattern.replace(/\*\*\//g, '(.*/)?').replace(/\*/g, '[^/]*').replace(/\?/g, '.');
2167
2185
  if (new RegExp(globPattern, 'i').test(basename) || fullPath.includes(pattern.replace(/\*/g, ''))) {
2168
2186
  files.push(fullPath);
2169
2187
  }
@@ -2247,7 +2265,9 @@ class AgenticTools {
2247
2265
  if (includePattern)
2248
2266
  rgArgs.push('-g', includePattern);
2249
2267
  rgArgs.push('--', query, this.cwd);
2250
- const rgOutput = (0, child_process_1.execSync)(`rg ${rgArgs.map(a => `'${a}'`).join(' ')}`, {
2268
+ const isWin = process.platform === 'win32';
2269
+ const quote = (s) => isWin ? `"${s}"` : `'${s}'`;
2270
+ const rgOutput = (0, child_process_1.execSync)(`rg ${rgArgs.map(a => quote(a)).join(' ')}`, {
2251
2271
  encoding: 'utf-8',
2252
2272
  timeout: 15000,
2253
2273
  maxBuffer: 5 * 1024 * 1024,
@@ -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.52",
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
+ }