vigthoria-cli 1.11.11 → 1.11.17
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/dist/commands/chat.d.ts +10 -0
- package/dist/commands/chat.js +281 -24
- package/dist/utils/agentRoute.d.ts +17 -0
- package/dist/utils/agentRoute.js +137 -0
- package/dist/utils/api.d.ts +2 -1
- package/dist/utils/api.js +71 -73
- package/dist/utils/deckEvents.d.ts +17 -0
- package/dist/utils/deckEvents.js +35 -0
- package/dist/utils/fastAgentRouter.d.ts +34 -0
- package/dist/utils/fastAgentRouter.js +171 -0
- package/dist/utils/requestIntent.d.ts +34 -0
- package/dist/utils/requestIntent.js +170 -0
- package/dist/utils/templateInstantPath.d.ts +18 -0
- package/dist/utils/templateInstantPath.js +121 -0
- package/dist/utils/v3-workspace-path.d.ts +7 -0
- package/dist/utils/v3-workspace-path.js +85 -0
- package/dist/utils/workspace-stream.js +9 -2
- package/package.json +2 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared prompt intent detection for CLI routing, V3 hints, and rescue gating.
|
|
3
|
+
* Keep aligned with V3-Code-Agent/agent.py _classify_request heuristics.
|
|
4
|
+
*/
|
|
5
|
+
const CLI_SHAPING_BLOCK = /(?:^|\n\n)Platform:\s*(?:Windows|macOS|Linux)\.[\s\S]*?(?=\n\n[A-Z][^\n]{0,40}:|\s*$)/i;
|
|
6
|
+
const READONLY_SHAPING_BLOCK = /(?:^|\n\n)(?:Read-only analysis mode is active\.|Diagnostic mode is active\.)[\s\S]*?(?=\n\n[A-Z][^\n]{0,40}:|\s*$)/i;
|
|
7
|
+
const BUILD_VERBS = /\b(build|create|make|implement|complete|fix|repair|edit|modify|write|generate|add|finish|scaffold|develop|update|change|refactor|let's|let us|we need|we should|erstelle|erstellen|schreib|schreibe|bearbeite)\b/i;
|
|
8
|
+
const ARTIFACT_NOUNS = /\b(file|files|project|game|spiel|app|website|html5|frontend|component|feature|code|workspace|repo|clone|replica|remake|page|site|canvas|sprite|level|levels|enemy|enemies|character|npc|world|scene|script|module|api|database|config|landing|dashboard|platformer|arcade|playable|collectible|scoreboard|leaderboard|pitfall|pacman|rogue|html|css|javascript|typescript)\b/i;
|
|
9
|
+
/** Canvas/playable games only — bare "html5" or "website" must NOT match. */
|
|
10
|
+
const GAME_INTENT = /\b(game|spiel|playable|html5\s+(?:game|canvas)|html5\s+game|canvas\s+game|game\s+canvas|arcade|platformer|side[- ]?scroller|pitfall|pac[- ]?man|tetris|snake|breakout|pong|roguelike|metroidvania|tower\s+defense|clone\s+of\s+(?:a\s+)?(?:game|pitfall|pac|mario|zelda|arcade)|gameplay|collectible|boss\s+fight|level\s+design|sprite\s+sheet|wild\s+(?:boar|pig|forest)|forest\s+pig)\b/i;
|
|
11
|
+
const WEB_PAGE_INTENT = /\b(website|web\s*site|webpage|web\s*page|landing\s+page|home\s*page|index\.html|html\s+page|static\s+page|single\s+page|popup|alert|modal|hello\s+world)\b/i;
|
|
12
|
+
const CLONE_BUILD = /\bclone\s+of\b|\b(build|create|make)\s+(?:a|an|the|us|me)?\s*(?:clone|replica|remake|port)\b/i;
|
|
13
|
+
const READ_ONLY_INTENT = /\b(analy[sz]e|analyse|analysis|audit|review|inspect|proof|understand|summari[sz]e|scan|read[\s-]?only|where we left|what is|how does|show me|tell me|identify\s+gaps?|gap\s+analysis|production\s+blockers?)\b/i;
|
|
14
|
+
const EXPLICIT_WRITE_VERBS = /\b(implement|write|edit|modify|update|fix|repair|create|build|generate|scaffold|deploy|refactor|develop|integrate)\b/i;
|
|
15
|
+
const TRIVIAL_HTML_PAGE = /\b(hello\s*world|simple\s+(?:html|web|page|site)|html5\s+website|html\s+5\s+website|popup|alert\s*\(|show\s+a\s+popup|one\s+page|single\s+html)\b/i;
|
|
16
|
+
/** Strip CLI-appended platform / mode shaping so it cannot flip quality profiles. */
|
|
17
|
+
export function stripExecutionShaping(prompt) {
|
|
18
|
+
return String(prompt || '')
|
|
19
|
+
.replace(CLI_SHAPING_BLOCK, '')
|
|
20
|
+
.replace(READONLY_SHAPING_BLOCK, '')
|
|
21
|
+
.trim();
|
|
22
|
+
}
|
|
23
|
+
export function hasBuildVerb(prompt) {
|
|
24
|
+
return BUILD_VERBS.test(stripExecutionShaping(prompt));
|
|
25
|
+
}
|
|
26
|
+
export function hasArtifactNoun(prompt) {
|
|
27
|
+
return ARTIFACT_NOUNS.test(stripExecutionShaping(prompt));
|
|
28
|
+
}
|
|
29
|
+
/** Game clones require a game target — generic "clone of X" must not imply game-build. */
|
|
30
|
+
const GAME_CLONE_TARGET = /\bclone\s+of\s+(?:a\s+)?(?:the\s+)?(?:game|pitfall|pac[- ]?man|mario|zelda|tetris|snake|arcade|spiel)\b/i;
|
|
31
|
+
export function hasGameIntent(prompt) {
|
|
32
|
+
const text = stripExecutionShaping(prompt);
|
|
33
|
+
if (GAME_INTENT.test(text)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (GAME_CLONE_TARGET.test(text)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (CLONE_BUILD.test(text) && /\b(game|spiel|pitfall|pacman|arcade|platformer|playable|canvas)\b/i.test(text)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
export function isContinueOrRetryPrompt(prompt) {
|
|
45
|
+
const text = stripExecutionShaping(prompt).toLowerCase();
|
|
46
|
+
return /\b(continue\s+(?:the\s+)?(?:previous|last|current)\s+(?:agent\s+)?run|\/retry|\/continue|resume\s+(?:the\s+)?(?:failed|unfinished)|planner\s+produced\s+\d+\s+execution\s+tasks)\b/i.test(text);
|
|
47
|
+
}
|
|
48
|
+
export function hasWebPageIntent(prompt) {
|
|
49
|
+
const text = stripExecutionShaping(prompt);
|
|
50
|
+
return WEB_PAGE_INTENT.test(text) || /\bhtml5\s+website\b/i.test(text);
|
|
51
|
+
}
|
|
52
|
+
export function isTrivialHtmlPageRequest(prompt) {
|
|
53
|
+
const text = stripExecutionShaping(prompt);
|
|
54
|
+
if (isContinueOrRetryPrompt(prompt)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (!hasBuildVerb(prompt) && !/\b(write|create|make|show)\b/i.test(text)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (hasGameIntent(prompt)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (text.length > 420) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (/\b(multi[- ]?page|full\s+saas|dashboard|e[\s-]?commerce|auth|database|backend|api|microservice)\b/i.test(text)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return TRIVIAL_HTML_PAGE.test(text)
|
|
70
|
+
|| (hasWebPageIntent(prompt) && /\b(html5|html5\s+website|website|popup|hello\s*world)\b/i.test(text));
|
|
71
|
+
}
|
|
72
|
+
export function hasCloneBuildIntent(prompt) {
|
|
73
|
+
return CLONE_BUILD.test(stripExecutionShaping(prompt)) && hasBuildVerb(prompt);
|
|
74
|
+
}
|
|
75
|
+
export function hasExplicitWriteIntent(prompt) {
|
|
76
|
+
const text = stripExecutionShaping(prompt);
|
|
77
|
+
return (BUILD_VERBS.test(text) && ARTIFACT_NOUNS.test(text))
|
|
78
|
+
|| hasCloneBuildIntent(prompt)
|
|
79
|
+
|| (BUILD_VERBS.test(text) && hasGameIntent(prompt));
|
|
80
|
+
}
|
|
81
|
+
export function taskRequiresWorkspaceChanges(prompt) {
|
|
82
|
+
const text = String(prompt || '').trim();
|
|
83
|
+
const shaped = stripExecutionShaping(text);
|
|
84
|
+
const readOnlyIntent = READ_ONLY_INTENT.test(shaped);
|
|
85
|
+
const explicitWriteIntent = hasExplicitWriteIntent(text);
|
|
86
|
+
if (readOnlyIntent && !explicitWriteIntent) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return explicitWriteIntent;
|
|
90
|
+
}
|
|
91
|
+
export function inferAgentTaskType(prompt) {
|
|
92
|
+
const text = stripExecutionShaping(prompt);
|
|
93
|
+
if (/\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(text)
|
|
94
|
+
&& !hasExplicitWriteIntent(prompt)) {
|
|
95
|
+
return 'debugging';
|
|
96
|
+
}
|
|
97
|
+
if (/\b(analy[sz]e|analyse|audit|review|inspect|identify\s+gaps?|gap\s+analysis|production\s+blockers?|read[\s-]?only)\b/i.test(text)
|
|
98
|
+
&& !hasExplicitWriteIntent(prompt)) {
|
|
99
|
+
return 'analysis';
|
|
100
|
+
}
|
|
101
|
+
if (hasBuildVerb(prompt) && !/\b(analy[sz]e|analyse|audit|review|inspect)\b/i.test(text)) {
|
|
102
|
+
if (hasGameIntent(prompt)) {
|
|
103
|
+
return 'game-build';
|
|
104
|
+
}
|
|
105
|
+
if (hasWebPageIntent(prompt)) {
|
|
106
|
+
return 'web-build';
|
|
107
|
+
}
|
|
108
|
+
return 'implementation';
|
|
109
|
+
}
|
|
110
|
+
return 'analysis';
|
|
111
|
+
}
|
|
112
|
+
export function shouldSkipAnalysisRescue(liveOutcome) {
|
|
113
|
+
if (liveOutcome.requiresWorkspaceChanges) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if ((liveOutcome.tasksTotal || 0) > 0) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if ((liveOutcome.changedFileCount || 0) > 0) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
export function isPlannerBuildTask(agentTaskType, prompt) {
|
|
125
|
+
const kind = String(agentTaskType || '').toLowerCase();
|
|
126
|
+
if (isTrivialHtmlPageRequest(prompt)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (/^(game-build|web-build|implementation|build|repair|refactor-build)$/.test(kind)) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return hasExplicitWriteIntent(prompt) && !READ_ONLY_INTENT.test(stripExecutionShaping(prompt));
|
|
133
|
+
}
|
|
134
|
+
export function resolvePlannerAgentTimeoutMs(baseTimeoutMs, agentTaskType, prompt) {
|
|
135
|
+
if (!isPlannerBuildTask(agentTaskType, prompt)) {
|
|
136
|
+
return baseTimeoutMs;
|
|
137
|
+
}
|
|
138
|
+
const raw = process.env.VIGTHORIA_AGENT_PLANNER_TIMEOUT_MS || process.env.V3_AGENT_PLANNER_TIMEOUT_MS || '900000';
|
|
139
|
+
const extended = Number.parseInt(raw, 10);
|
|
140
|
+
const floor = Number.isFinite(extended) && extended > 0 ? extended : 900000;
|
|
141
|
+
return Math.max(baseTimeoutMs, floor);
|
|
142
|
+
}
|
|
143
|
+
/** V3 workflow mode: suppress write path when the user did not ask for file changes. */
|
|
144
|
+
export function resolveWorkflowType(agentTaskType, prompt) {
|
|
145
|
+
const kind = String(agentTaskType || '').toLowerCase();
|
|
146
|
+
if (kind === 'analysis' || kind === 'debugging' || kind === 'verification') {
|
|
147
|
+
return 'analysis_only';
|
|
148
|
+
}
|
|
149
|
+
if (!taskRequiresWorkspaceChanges(prompt)) {
|
|
150
|
+
return 'analysis_only';
|
|
151
|
+
}
|
|
152
|
+
return 'full';
|
|
153
|
+
}
|
|
154
|
+
export function buildExecutionHints(agentTaskType, prompt, options = {}) {
|
|
155
|
+
const workflowType = resolveWorkflowType(agentTaskType, prompt);
|
|
156
|
+
const kind = String(agentTaskType || 'general').toLowerCase();
|
|
157
|
+
const hints = {
|
|
158
|
+
task_kind: kind,
|
|
159
|
+
requires_file_changes: workflowType === 'analysis_only' ? false : undefined,
|
|
160
|
+
classify_from: String(prompt || ''),
|
|
161
|
+
workflow_type: workflowType,
|
|
162
|
+
};
|
|
163
|
+
if (options.fastPath) {
|
|
164
|
+
hints.fast_path = options.fastPath;
|
|
165
|
+
}
|
|
166
|
+
else if (isTrivialHtmlPageRequest(prompt)) {
|
|
167
|
+
hints.fast_path = 'template-instant';
|
|
168
|
+
}
|
|
169
|
+
return hints;
|
|
170
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-second path: Template Service match → write index.html (skip V3 planner).
|
|
3
|
+
*/
|
|
4
|
+
import type { APIClient } from './api.js';
|
|
5
|
+
export type TemplateInstantResult = {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
entryPath?: string;
|
|
8
|
+
templateName?: string;
|
|
9
|
+
confidence?: number;
|
|
10
|
+
processingMs?: number;
|
|
11
|
+
error?: string;
|
|
12
|
+
usedFallback?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function runTemplateInstantPath(api: APIClient, vision: string, workspacePath: string, options?: {
|
|
15
|
+
entryPath?: string;
|
|
16
|
+
allowFallback?: boolean;
|
|
17
|
+
forceMinimalFallback?: boolean;
|
|
18
|
+
}): Promise<TemplateInstantResult>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-second path: Template Service match → write index.html (skip V3 planner).
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
function ensurePopupScript(html, vision) {
|
|
7
|
+
const text = String(vision || '').toLowerCase();
|
|
8
|
+
if (!/\b(popup|alert|hello\s*world)\b/.test(text)) {
|
|
9
|
+
return html;
|
|
10
|
+
}
|
|
11
|
+
if (/\balert\s*\(/i.test(html)) {
|
|
12
|
+
return html;
|
|
13
|
+
}
|
|
14
|
+
const script = "<script>window.addEventListener('load',()=>alert('Hello World'));</script>";
|
|
15
|
+
if (/<\/body>/i.test(html)) {
|
|
16
|
+
return html.replace(/<\/body>/i, `${script}\n</body>`);
|
|
17
|
+
}
|
|
18
|
+
return `${html}\n${script}`;
|
|
19
|
+
}
|
|
20
|
+
function minimalHelloWorldHtml(popupMessage = 'Hello World') {
|
|
21
|
+
const safe = String(popupMessage || 'Hello World').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
22
|
+
return `<!DOCTYPE html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="UTF-8">
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
27
|
+
<title>Hello World</title>
|
|
28
|
+
<style>
|
|
29
|
+
body { font-family: system-ui, sans-serif; display: grid; place-items: center; min-height: 100vh; margin: 0; background: #0a0a1a; color: #e6f0ff; }
|
|
30
|
+
h1 { font-size: 2rem; }
|
|
31
|
+
</style>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<h1>Hello World</h1>
|
|
35
|
+
<script>window.addEventListener('load', () => alert('${safe}'));</script>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
async function fetchTemplateMatch(api, vision, timeoutMs = 15000) {
|
|
41
|
+
const bases = api.getTemplateServiceBaseUrls();
|
|
42
|
+
let lastError = 'Template Service unreachable';
|
|
43
|
+
for (const base of bases) {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(`${base}/match`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({ vision }),
|
|
51
|
+
signal: controller.signal,
|
|
52
|
+
});
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
const payload = await response.json().catch(() => null);
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
lastError = payload?.error || `HTTP ${response.status} from ${base}`;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!payload?.success || !payload?.html) {
|
|
60
|
+
lastError = payload?.message || payload?.error || 'No template match';
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const templateMeta = payload.template;
|
|
64
|
+
const templateName = typeof templateMeta === 'string'
|
|
65
|
+
? templateMeta
|
|
66
|
+
: (templateMeta?.name || templateMeta?.id);
|
|
67
|
+
return {
|
|
68
|
+
html: String(payload.html),
|
|
69
|
+
template: templateName,
|
|
70
|
+
confidence: Number(payload.confidence) || 0,
|
|
71
|
+
processingMs: Number(payload.processing_time || payload.processingTime) || 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
lastError = err?.message || String(err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { error: lastError };
|
|
80
|
+
}
|
|
81
|
+
function extractPopupMessage(vision) {
|
|
82
|
+
const quoted = vision.match(/popup\s+with\s+["']([^"']+)["']/i);
|
|
83
|
+
if (quoted?.[1])
|
|
84
|
+
return quoted[1];
|
|
85
|
+
if (/\bhello\s*world\b/i.test(vision))
|
|
86
|
+
return 'Hello World';
|
|
87
|
+
return 'Hello World';
|
|
88
|
+
}
|
|
89
|
+
export async function runTemplateInstantPath(api, vision, workspacePath, options = {}) {
|
|
90
|
+
const entryPath = options.entryPath || 'index.html';
|
|
91
|
+
const target = path.join(workspacePath, entryPath);
|
|
92
|
+
let html;
|
|
93
|
+
let usedFallback = false;
|
|
94
|
+
let match = {};
|
|
95
|
+
if (options.forceMinimalFallback) {
|
|
96
|
+
html = minimalHelloWorldHtml(extractPopupMessage(vision));
|
|
97
|
+
usedFallback = true;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
match = await fetchTemplateMatch(api, vision);
|
|
101
|
+
html = match.html;
|
|
102
|
+
if (!html && options.allowFallback !== false) {
|
|
103
|
+
html = minimalHelloWorldHtml(extractPopupMessage(vision));
|
|
104
|
+
usedFallback = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!html) {
|
|
108
|
+
return { ok: false, error: match.error || 'Template match failed' };
|
|
109
|
+
}
|
|
110
|
+
html = ensurePopupScript(html, vision);
|
|
111
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
112
|
+
fs.writeFileSync(target, html, 'utf8');
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
entryPath,
|
|
116
|
+
templateName: match.template || (usedFallback ? 'minimal-hello-world' : undefined),
|
|
117
|
+
confidence: match.confidence,
|
|
118
|
+
processingMs: match.processingMs,
|
|
119
|
+
usedFallback,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize V3 SSE / tool paths to a safe workspace-relative POSIX path.
|
|
3
|
+
* Decodes vigthoria:// boundary URIs from server scrubbing before local writes.
|
|
4
|
+
*/
|
|
5
|
+
export declare function normalizeV3WorkspaceRelativePath(rawPath: string, rootPath?: string): string;
|
|
6
|
+
/** Join workspace root + relative path without path.resolve URI corruption on Windows. */
|
|
7
|
+
export declare function joinV3WorkspacePath(rootPath: string, relativePath: string): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize V3 SSE / tool paths to a safe workspace-relative POSIX path.
|
|
4
|
+
* Decodes vigthoria:// boundary URIs from server scrubbing before local writes.
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
|
|
7
|
+
let input = String(rawPath || '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
8
|
+
if (!input) {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
const safeRelative = (candidate) => {
|
|
12
|
+
const stripped = String(candidate || '').replace(/^\/+/, '');
|
|
13
|
+
if (!stripped) {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
if (process.platform === 'win32' && /:/.test(stripped) && !/^[a-zA-Z]:/.test(stripped)) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
if (/^[a-zA-Z]:\//.test(stripped)) {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
const normalized = path.posix.normalize(stripped);
|
|
23
|
+
if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../') || path.posix.isAbsolute(normalized)) {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
return normalized;
|
|
27
|
+
};
|
|
28
|
+
const decodeBoundaryUri = (value) => {
|
|
29
|
+
const match = value.match(/^vigthoria:\/\/(?:workspace|server-internal)\/?(.*)$/i);
|
|
30
|
+
if (match) {
|
|
31
|
+
return safeRelative(match[1] || '');
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
const decoded = decodeBoundaryUri(input);
|
|
36
|
+
if (decoded !== null) {
|
|
37
|
+
return decoded;
|
|
38
|
+
}
|
|
39
|
+
if (/^vigthoria:\/\//i.test(input)) {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
const internalWorkspacePatterns = [
|
|
43
|
+
/^\/?var\/www\/\.vigthoria\/v3-temp\/vig-remote-[^/]+\/(.+)$/i,
|
|
44
|
+
/^\/?var\/www\/vigthoria:\/\/?server-internal\/?(.+)$/i,
|
|
45
|
+
/^var\/www\/vigthoria:\/\/?server-internal\/?(.+)$/i,
|
|
46
|
+
/^var\/www\/vigthoria:\/\/server-internal\/?(.+)$/i,
|
|
47
|
+
/^\.vigthoria\/v3-temp\/vig-remote-[^/]+\/(.+)$/i,
|
|
48
|
+
/^\/?tmp\/vig-remote(?:-server)?-[^/]+\/(.+)$/i,
|
|
49
|
+
/^\/?tmp\/vig-fork-[^/]+\/(.+)$/i,
|
|
50
|
+
/^\/?home\/user\/(.+)$/i,
|
|
51
|
+
/^\/?root\/(.+)$/i,
|
|
52
|
+
];
|
|
53
|
+
for (const pattern of internalWorkspacePatterns) {
|
|
54
|
+
const match = input.match(pattern);
|
|
55
|
+
if (match && match[1]) {
|
|
56
|
+
return safeRelative(match[1]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const normalizedRoot = String(rootPath || '').trim().replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
60
|
+
if (normalizedRoot) {
|
|
61
|
+
const rootNoLeadingSlash = normalizedRoot.replace(/^\//, '');
|
|
62
|
+
const rootBase = path.posix.basename(normalizedRoot);
|
|
63
|
+
const prefixes = [
|
|
64
|
+
`${normalizedRoot}/`,
|
|
65
|
+
`${rootNoLeadingSlash}/`,
|
|
66
|
+
`${rootBase}/`,
|
|
67
|
+
];
|
|
68
|
+
for (const prefix of prefixes) {
|
|
69
|
+
if (input.startsWith(prefix)) {
|
|
70
|
+
return safeRelative(input.slice(prefix.length));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const embeddedRoot = `/${rootBase}/`;
|
|
74
|
+
const embeddedIndex = input.indexOf(embeddedRoot);
|
|
75
|
+
if (embeddedIndex >= 0) {
|
|
76
|
+
return safeRelative(input.slice(embeddedIndex + embeddedRoot.length));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return safeRelative(input);
|
|
80
|
+
}
|
|
81
|
+
/** Join workspace root + relative path without path.resolve URI corruption on Windows. */
|
|
82
|
+
export function joinV3WorkspacePath(rootPath, relativePath) {
|
|
83
|
+
const segments = relativePath.split('/').filter((segment) => segment && segment !== '.');
|
|
84
|
+
return path.join(rootPath, ...segments);
|
|
85
|
+
}
|
|
@@ -22,6 +22,7 @@ import fs from 'fs';
|
|
|
22
22
|
import path from 'path';
|
|
23
23
|
import WebSocket from 'ws';
|
|
24
24
|
import { Logger } from './logger.js';
|
|
25
|
+
import { joinV3WorkspacePath, normalizeV3WorkspaceRelativePath } from './v3-workspace-path.js';
|
|
25
26
|
const logger = new Logger();
|
|
26
27
|
logger.setVerbose(!!process.env.VIGTHORIA_DEBUG);
|
|
27
28
|
// Files/dirs to ignore in the watcher
|
|
@@ -74,9 +75,15 @@ export function applyFileMutation(event, workspaceRoot) {
|
|
|
74
75
|
return false;
|
|
75
76
|
if (!event.path || !workspaceRoot)
|
|
76
77
|
return false;
|
|
77
|
-
const
|
|
78
|
+
const relativePath = normalizeV3WorkspaceRelativePath(event.path, workspaceRoot);
|
|
79
|
+
if (!relativePath) {
|
|
80
|
+
logger.warn(`Refusing to apply mutation with invalid path: ${event.path}`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const absPath = joinV3WorkspacePath(workspaceRoot, relativePath);
|
|
84
|
+
const resolvedRoot = path.resolve(workspaceRoot);
|
|
78
85
|
// Safety: ensure the resolved path is within the workspace
|
|
79
|
-
if (!absPath.startsWith(
|
|
86
|
+
if (!absPath.startsWith(resolvedRoot + path.sep) && absPath !== resolvedRoot) {
|
|
80
87
|
logger.warn(`Refusing to apply mutation outside workspace: ${event.path}`);
|
|
81
88
|
return false;
|
|
82
89
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigthoria-cli",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.17",
|
|
4
4
|
"description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"test:regression": "npm run build && node scripts/test-regression-1.6.22.js",
|
|
42
42
|
"test:agent:smoke": "npm run build && node scripts/test-agent-smoke.js",
|
|
43
43
|
"test:agent:routing": "npm run build && node scripts/test-agent-routing-policy.js",
|
|
44
|
+
"test:agent:route": "npm run build && node scripts/test-request-intent.js && node scripts/test-agent-route.js && node scripts/test-local-use-cases.js",
|
|
44
45
|
"test:agent:context": "npm run build && node scripts/test-agent-context-trace-e2e.js",
|
|
45
46
|
"test:mcp:context": "npm run build && node scripts/test-mcp-context-session-e2e.js",
|
|
46
47
|
"test:workflow:surface": "npm run build && node scripts/test-workflow-surface-e2e.js",
|