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 +6 -1
- package/dist/commands/chat.js +20 -1
- package/dist/utils/api.js +14 -0
- package/dist/utils/tools.d.ts +1 -1
- package/dist/utils/tools.js +28 -8
- package/dist/utils/workspace-stream.d.ts +75 -0
- package/dist/utils/workspace-stream.js +301 -0
- package/package.json +3 -2
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.
|
|
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):**
|
package/dist/commands/chat.js
CHANGED
|
@@ -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
|
package/dist/utils/tools.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/utils/tools.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2093
|
-
fs.writeFileSync(edit.resolvedPath,
|
|
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(
|
|
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
|
|
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.
|
|
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
|
+
}
|