vigthoria-cli 1.11.4 → 1.11.6
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 +3 -0
- package/dist/commands/chat.js +67 -9
- package/dist/index.js +141 -90
- package/dist/utils/agentRunOutcome.d.ts +8 -0
- package/dist/utils/agentRunOutcome.js +52 -4
- package/dist/utils/api.js +52 -12
- package/package.json +1 -1
package/dist/commands/chat.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export declare class ChatCommand {
|
|
|
47
47
|
private retryPromptSignature;
|
|
48
48
|
private retryPromptStreak;
|
|
49
49
|
private v3SuppressThinkingStream;
|
|
50
|
+
private v3StreamedAnswerBuffer;
|
|
50
51
|
private lastAgentRunOutcome;
|
|
51
52
|
private isJwtExpirationError;
|
|
52
53
|
private isNetworkError;
|
|
@@ -62,6 +63,8 @@ export declare class ChatCommand {
|
|
|
62
63
|
private getDefaultChatModel;
|
|
63
64
|
private resolveInitialModel;
|
|
64
65
|
private isLegacyAgentFallbackAllowed;
|
|
66
|
+
private shouldRescueFailedAnalysis;
|
|
67
|
+
private tryAnalysisLocalRescue;
|
|
65
68
|
private resolveAgentExecutionPolicy;
|
|
66
69
|
private getMessagesForModel;
|
|
67
70
|
private getActivePersonaMode;
|
package/dist/commands/chat.js
CHANGED
|
@@ -13,7 +13,7 @@ import { TaskDisplay } from '../utils/task-display.js';
|
|
|
13
13
|
import { ProjectMemoryService } from '../utils/project-memory.js';
|
|
14
14
|
import { buildPersonaOverlay, normalizePersonaMode } from '../utils/persona.js';
|
|
15
15
|
import { runAgentSessionMenu, shouldShowAgentSessionMenu } from './agent-session-menu.js';
|
|
16
|
-
import { createLiveOutcome, evaluateExecutorSuccess, handleTaskEvent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
|
|
16
|
+
import { createLiveOutcome, evaluateExecutorSuccess, handleTaskEvent, normalizeAgentAnswerContent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
|
|
17
17
|
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
18
18
|
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
|
|
19
19
|
if (!rawValue) {
|
|
@@ -66,6 +66,7 @@ export class ChatCommand {
|
|
|
66
66
|
retryPromptSignature = null;
|
|
67
67
|
retryPromptStreak = 0;
|
|
68
68
|
v3SuppressThinkingStream = false;
|
|
69
|
+
v3StreamedAnswerBuffer = '';
|
|
69
70
|
// Last completed Agent run — used by /retry, /continue, and the final summary block.
|
|
70
71
|
lastAgentRunOutcome = null;
|
|
71
72
|
isJwtExpirationError(error) {
|
|
@@ -258,6 +259,28 @@ export class ChatCommand {
|
|
|
258
259
|
// Operators can still opt in during emergency local debugging.
|
|
259
260
|
return process.env.VIGTHORIA_ALLOW_LEGACY_AGENT_FALLBACK === '1';
|
|
260
261
|
}
|
|
262
|
+
shouldRescueFailedAnalysis() {
|
|
263
|
+
return process.env.VIGTHORIA_DISABLE_ANALYSIS_RESCUE !== '1';
|
|
264
|
+
}
|
|
265
|
+
async tryAnalysisLocalRescue(prompt, workspacePath) {
|
|
266
|
+
if (!this.shouldRescueFailedAnalysis() || !this.tools) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
const clientToolErrors = this.api.getClientToolErrors();
|
|
270
|
+
if (clientToolErrors.some((err) => /could not accept the result|client tool bridge failed/i.test(err))) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log(chalk.yellow('V3 analysis did not produce a grounded answer — retrying with local workspace tools...'));
|
|
275
|
+
console.log(chalk.gray(`Workspace: ${workspacePath}`));
|
|
276
|
+
console.log('');
|
|
277
|
+
const last = this.messages[this.messages.length - 1];
|
|
278
|
+
if (last?.role === 'assistant') {
|
|
279
|
+
this.messages.pop();
|
|
280
|
+
}
|
|
281
|
+
await this.runLocalAgentLoop(prompt);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
261
284
|
resolveAgentExecutionPolicy(prompt) {
|
|
262
285
|
const explicitModel = this.modelExplicitlySelected;
|
|
263
286
|
const heavyTask = this.config.isComplexTask(prompt);
|
|
@@ -733,6 +756,7 @@ export class ChatCommand {
|
|
|
733
756
|
spinner.stop();
|
|
734
757
|
}
|
|
735
758
|
process.stdout.write(safeText);
|
|
759
|
+
this.v3StreamedAnswerBuffer += safeText;
|
|
736
760
|
}
|
|
737
761
|
sanitizeV3VisibleStreamText(text) {
|
|
738
762
|
let output = String(text || '');
|
|
@@ -1965,6 +1989,7 @@ export class ChatCommand {
|
|
|
1965
1989
|
qualityMissing: [],
|
|
1966
1990
|
qualityBlockers: evaluation.executorSucceeded ? [] : [evaluation.statusHeadline],
|
|
1967
1991
|
hasOutput: liveOutcome.workspaceHasOutput,
|
|
1992
|
+
answerContent: liveOutcome.answerContent || null,
|
|
1968
1993
|
selfHealStatus: 'skipped',
|
|
1969
1994
|
selfHealTool: null,
|
|
1970
1995
|
plannerError: null,
|
|
@@ -2453,6 +2478,7 @@ export class ChatCommand {
|
|
|
2453
2478
|
this.v3ToolCallCount = 0;
|
|
2454
2479
|
this.v3LastActivity = Date.now();
|
|
2455
2480
|
this.v3StreamingStarted = false;
|
|
2481
|
+
this.v3StreamedAnswerBuffer = '';
|
|
2456
2482
|
const taskDisplay = new TaskDisplay(['Analyse workspace', 'Execute tasks', 'Validate output', 'Self-heal'], !this.jsonOutput);
|
|
2457
2483
|
taskDisplay.start(0);
|
|
2458
2484
|
const spinner = this.jsonOutput ? null : createSpinner({
|
|
@@ -2637,9 +2663,11 @@ export class ChatCommand {
|
|
|
2637
2663
|
const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
|
|
2638
2664
|
const changedFileCount = response.changedFiles ? Object.keys(response.changedFiles).length : 0;
|
|
2639
2665
|
const requiresWorkspaceChanges = this.taskRequiresWorkspaceChanges(prompt);
|
|
2666
|
+
const answerContent = normalizeAgentAnswerContent(response.content, this.v3StreamedAnswerBuffer);
|
|
2640
2667
|
liveOutcome.changedFileCount = changedFileCount;
|
|
2641
2668
|
liveOutcome.requiresWorkspaceChanges = requiresWorkspaceChanges;
|
|
2642
|
-
liveOutcome.workspaceHasOutput = workspaceHasOutput;
|
|
2669
|
+
liveOutcome.workspaceHasOutput = requiresWorkspaceChanges ? workspaceHasOutput : false;
|
|
2670
|
+
liveOutcome.answerContent = answerContent;
|
|
2643
2671
|
const success = previewGate?.required === true
|
|
2644
2672
|
? (previewGate?.passed === true || workspaceHasOutput)
|
|
2645
2673
|
: true;
|
|
@@ -2683,12 +2711,27 @@ export class ChatCommand {
|
|
|
2683
2711
|
console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
|
|
2684
2712
|
}
|
|
2685
2713
|
}
|
|
2714
|
+
else if (answerContent) {
|
|
2715
|
+
if (!this.directPromptMode) {
|
|
2716
|
+
console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
|
|
2717
|
+
}
|
|
2718
|
+
console.log('');
|
|
2719
|
+
console.log(chalk.bold('Answer:'));
|
|
2720
|
+
console.log(answerContent);
|
|
2721
|
+
}
|
|
2686
2722
|
else if (response.content) {
|
|
2687
2723
|
if (!this.directPromptMode) {
|
|
2688
2724
|
console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
|
|
2689
2725
|
}
|
|
2690
2726
|
console.log(response.content);
|
|
2691
2727
|
}
|
|
2728
|
+
else if (!requiresWorkspaceChanges) {
|
|
2729
|
+
if (!this.directPromptMode) {
|
|
2730
|
+
console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
|
|
2731
|
+
}
|
|
2732
|
+
console.log(chalk.yellow('The agent finished without producing an answer to your question.'));
|
|
2733
|
+
console.log(chalk.gray('Try /retry or ask again with more detail (e.g. file name or feature).'));
|
|
2734
|
+
}
|
|
2692
2735
|
else {
|
|
2693
2736
|
if (!this.directPromptMode) {
|
|
2694
2737
|
console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
|
|
@@ -2723,7 +2766,14 @@ export class ChatCommand {
|
|
|
2723
2766
|
let selfHealStatus = null;
|
|
2724
2767
|
let selfHealTool = null;
|
|
2725
2768
|
const runEvaluation = evaluateExecutorSuccess(liveOutcome);
|
|
2726
|
-
|
|
2769
|
+
let executorSucceeded = runEvaluation.executorSucceeded;
|
|
2770
|
+
if (!executorSucceeded && !requiresWorkspaceChanges && !this.jsonOutput) {
|
|
2771
|
+
const rescued = await this.tryAnalysisLocalRescue(prompt, workspacePath);
|
|
2772
|
+
if (rescued) {
|
|
2773
|
+
watcher?.stop();
|
|
2774
|
+
return true;
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2727
2777
|
if (!executorSucceeded && requiresWorkspaceChanges && changedFileCount === 0 && !liveOutcome.executorError) {
|
|
2728
2778
|
const clientToolErrors = this.api.getClientToolErrors();
|
|
2729
2779
|
const transportErrors = this.api.getLastChatTransportErrors();
|
|
@@ -2810,6 +2860,7 @@ export class ChatCommand {
|
|
|
2810
2860
|
qualityMissing: liveOutcome.qualityMissing,
|
|
2811
2861
|
qualityBlockers: liveOutcome.qualityBlockers,
|
|
2812
2862
|
hasOutput: workspaceHasOutput,
|
|
2863
|
+
answerContent: answerContent || null,
|
|
2813
2864
|
selfHealStatus,
|
|
2814
2865
|
selfHealTool,
|
|
2815
2866
|
plannerError: liveOutcome.plannerError ? sanitizeUserFacingErrorText(liveOutcome.plannerError) : null,
|
|
@@ -2817,7 +2868,7 @@ export class ChatCommand {
|
|
|
2817
2868
|
clientToolErrors: this.api.getClientToolErrors(),
|
|
2818
2869
|
transportErrors: this.api.getLastChatTransportErrors(),
|
|
2819
2870
|
workspacePath: workspacePath || null,
|
|
2820
|
-
workspaceSyncIssue: (!executorSucceeded && workspaceHasOutput && changedFileCount === 0)
|
|
2871
|
+
workspaceSyncIssue: (!executorSucceeded && requiresWorkspaceChanges && workspaceHasOutput && changedFileCount === 0)
|
|
2821
2872
|
? 'Local workspace has files but the agent did not confirm tracked changes.'
|
|
2822
2873
|
: null,
|
|
2823
2874
|
finishedAt: Date.now(),
|
|
@@ -2837,7 +2888,7 @@ export class ChatCommand {
|
|
|
2837
2888
|
taskId: response.taskId || null,
|
|
2838
2889
|
contextId: response.contextId || null,
|
|
2839
2890
|
partial: response.partial === true || (!executorSucceeded && liveOutcome.tasksSucceeded > 0),
|
|
2840
|
-
content: response.content || runEvaluation.statusHeadline,
|
|
2891
|
+
content: answerContent || response.content || runEvaluation.statusHeadline,
|
|
2841
2892
|
statusHeadline: runEvaluation.statusHeadline,
|
|
2842
2893
|
tasksSucceeded: liveOutcome.tasksSucceeded,
|
|
2843
2894
|
tasksTotal: liveOutcome.tasksTotal,
|
|
@@ -2853,7 +2904,7 @@ export class ChatCommand {
|
|
|
2853
2904
|
},
|
|
2854
2905
|
}, null, 2));
|
|
2855
2906
|
}
|
|
2856
|
-
this.messages.push({ role: 'assistant', content: response.content || runEvaluation.statusHeadline });
|
|
2907
|
+
this.messages.push({ role: 'assistant', content: answerContent || response.content || runEvaluation.statusHeadline });
|
|
2857
2908
|
watcher?.stop();
|
|
2858
2909
|
return true;
|
|
2859
2910
|
}
|
|
@@ -2907,6 +2958,7 @@ export class ChatCommand {
|
|
|
2907
2958
|
qualityMissing: liveOutcome.qualityMissing,
|
|
2908
2959
|
qualityBlockers: liveOutcome.qualityBlockers,
|
|
2909
2960
|
hasOutput: this.api.hasAgentWorkspaceOutput(workspaceContext),
|
|
2961
|
+
answerContent: null,
|
|
2910
2962
|
selfHealStatus: 'skipped',
|
|
2911
2963
|
selfHealTool: null,
|
|
2912
2964
|
plannerError: liveOutcome.plannerError ? sanitizeUserFacingErrorText(liveOutcome.plannerError) : null,
|
|
@@ -3329,14 +3381,20 @@ export class ChatCommand {
|
|
|
3329
3381
|
: `${outcome.tasksSucceeded} task update${outcome.tasksSucceeded === 1 ? '' : 's'} reported`;
|
|
3330
3382
|
console.log(' ' + chalk.white(taskLine + '.'));
|
|
3331
3383
|
}
|
|
3384
|
+
else if (outcome.answerContent) {
|
|
3385
|
+
const preview = outcome.answerContent.length > 160
|
|
3386
|
+
? `${outcome.answerContent.slice(0, 160)}…`
|
|
3387
|
+
: outcome.answerContent;
|
|
3388
|
+
console.log(' ' + chalk.white(`Answer: ${preview}`));
|
|
3389
|
+
}
|
|
3332
3390
|
else if (changedFileCount > 0) {
|
|
3333
|
-
console.log(' ' + chalk.white(
|
|
3391
|
+
console.log(' ' + chalk.white('Workspace changes were produced and are ready for review.'));
|
|
3334
3392
|
}
|
|
3335
|
-
else if (outcome.hasOutput) {
|
|
3393
|
+
else if (outcome.hasOutput && executorSucceeded && changedFileCount === 0) {
|
|
3336
3394
|
console.log(' ' + chalk.white('The assistant returned a final answer for your request.'));
|
|
3337
3395
|
}
|
|
3338
3396
|
else {
|
|
3339
|
-
console.log(' ' + chalk.white('No final workspace output was confirmed.'));
|
|
3397
|
+
console.log(' ' + chalk.white('No final answer or workspace output was confirmed.'));
|
|
3340
3398
|
}
|
|
3341
3399
|
if (outcome.selfHealStatus && outcome.selfHealStatus !== 'skipped') {
|
|
3342
3400
|
console.log(' ' + chalk.white(`Self-check status: ${outcome.selfHealStatus}.`));
|
package/dist/index.js
CHANGED
|
@@ -75,8 +75,41 @@ function getPackageMetadataPaths() {
|
|
|
75
75
|
path.join(process.env.APPDATA || '', 'npm', 'node_modules', 'vigthoria-cli', 'package.json'),
|
|
76
76
|
];
|
|
77
77
|
}
|
|
78
|
+
function resolveRunningPackageJsonPath() {
|
|
79
|
+
const candidates = [];
|
|
80
|
+
const scriptArg = process.argv[1];
|
|
81
|
+
if (scriptArg) {
|
|
82
|
+
try {
|
|
83
|
+
const resolvedScript = fs.realpathSync(scriptArg);
|
|
84
|
+
candidates.push(path.join(path.dirname(resolvedScript), '..', 'package.json'));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
candidates.push(path.join(path.dirname(scriptArg), '..', 'package.json'));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
candidates.push(path.join(__dirname, '..', 'package.json'));
|
|
91
|
+
for (const candidate of candidates) {
|
|
92
|
+
try {
|
|
93
|
+
if (!fs.existsSync(candidate))
|
|
94
|
+
continue;
|
|
95
|
+
const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
96
|
+
if (pkg?.name === 'vigthoria-cli')
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// try next candidate
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
78
105
|
function getVersion() {
|
|
79
106
|
try {
|
|
107
|
+
const runningPackageJson = resolveRunningPackageJsonPath();
|
|
108
|
+
if (runningPackageJson) {
|
|
109
|
+
const pkg = JSON.parse(fs.readFileSync(runningPackageJson, 'utf8'));
|
|
110
|
+
if (pkg.version)
|
|
111
|
+
return pkg.version;
|
|
112
|
+
}
|
|
80
113
|
for (const p of getPackageMetadataPaths()) {
|
|
81
114
|
if (fs.existsSync(p)) {
|
|
82
115
|
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
@@ -149,6 +182,30 @@ function compareVersions(v1, v2) {
|
|
|
149
182
|
}
|
|
150
183
|
return 0;
|
|
151
184
|
}
|
|
185
|
+
function maxVersion(...versions) {
|
|
186
|
+
let best = null;
|
|
187
|
+
for (const version of versions) {
|
|
188
|
+
if (!version)
|
|
189
|
+
continue;
|
|
190
|
+
if (!best || compareVersions(version, best) > 0)
|
|
191
|
+
best = version;
|
|
192
|
+
}
|
|
193
|
+
return best;
|
|
194
|
+
}
|
|
195
|
+
async function fetchNpmLatestVersion() {
|
|
196
|
+
const { execSync } = await import('child_process');
|
|
197
|
+
try {
|
|
198
|
+
const latest = execSync('npm view vigthoria-cli version', {
|
|
199
|
+
encoding: 'utf8',
|
|
200
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
201
|
+
windowsHide: true,
|
|
202
|
+
}).trim();
|
|
203
|
+
return latest || null;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
152
209
|
const VERSION = getVersion();
|
|
153
210
|
const VIGTHORIA_DEFAULT_MANIFEST_URL = process.env.VIGTHORIA_UPDATE_MANIFEST_URL || "https://coder.vigthoria.io/releases/manifest.json";
|
|
154
211
|
function resolveManifestEntry(manifest, channel) {
|
|
@@ -1452,7 +1509,6 @@ Examples:
|
|
|
1452
1509
|
.option('--channel <name>', 'Release channel to use from manifest (default: stable)', 'stable')
|
|
1453
1510
|
.option('--allow-downgrade', 'Allow installing an older version from custom update source')
|
|
1454
1511
|
.action(async (options) => {
|
|
1455
|
-
const { execSync } = await import('child_process');
|
|
1456
1512
|
const updateTarget = typeof options.from === 'string' ? options.from.trim() : '';
|
|
1457
1513
|
const manifestUrl = typeof options.manifest === 'string' ? options.manifest.trim() : VIGTHORIA_DEFAULT_MANIFEST_URL.trim();
|
|
1458
1514
|
const channel = typeof options.channel === 'string' ? options.channel.trim() : 'stable';
|
|
@@ -1484,11 +1540,13 @@ Examples:
|
|
|
1484
1540
|
return;
|
|
1485
1541
|
}
|
|
1486
1542
|
}
|
|
1543
|
+
if (isOfflineMode()) {
|
|
1544
|
+
console.log(chalk.yellow('Offline mode (VIGTHORIA_OFFLINE=1): skipping update check.'));
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
const currentVersion = VERSION;
|
|
1548
|
+
let manifestEntry = null;
|
|
1487
1549
|
if (manifestUrl) {
|
|
1488
|
-
if (isOfflineMode()) {
|
|
1489
|
-
console.log(chalk.yellow('Offline mode (VIGTHORIA_OFFLINE=1): skipping manifest update.'));
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
1550
|
try {
|
|
1493
1551
|
console.log(chalk.cyan(`Checking manifest channel ${channel}...`));
|
|
1494
1552
|
const response = await axios.get(manifestUrl, {
|
|
@@ -1496,103 +1554,96 @@ Examples:
|
|
|
1496
1554
|
maxRedirects: 5,
|
|
1497
1555
|
validateStatus: (status) => status >= 200 && status < 300,
|
|
1498
1556
|
});
|
|
1499
|
-
|
|
1500
|
-
if (!
|
|
1501
|
-
console.
|
|
1502
|
-
|
|
1503
|
-
process.exitCode = 1;
|
|
1504
|
-
return;
|
|
1505
|
-
}
|
|
1506
|
-
const currentVersion = VERSION;
|
|
1507
|
-
const comparison = compareVersions(entry.version, currentVersion);
|
|
1508
|
-
if (!allowDowngrade && comparison <= 0) {
|
|
1509
|
-
console.log(chalk.green(`You are running the latest version for channel ${channel} (${currentVersion})`));
|
|
1510
|
-
return;
|
|
1511
|
-
}
|
|
1512
|
-
console.log(chalk.yellow(`Update available from manifest: ${currentVersion} -> ${entry.version} (${channel})`));
|
|
1513
|
-
if (entry.notes) {
|
|
1514
|
-
console.log(chalk.gray(` ${entry.notes}`));
|
|
1515
|
-
}
|
|
1516
|
-
if (options.check) {
|
|
1517
|
-
console.log(chalk.gray(`Install with: vigthoria update --manifest ${manifestUrl} --channel ${channel}`));
|
|
1518
|
-
return;
|
|
1519
|
-
}
|
|
1520
|
-
const tmpFile = path.join(os.tmpdir(), `vigthoria-update-${Date.now()}.tgz`);
|
|
1521
|
-
try {
|
|
1522
|
-
console.log(chalk.cyan('Downloading release package...'));
|
|
1523
|
-
await downloadFile(entry.url, tmpFile);
|
|
1524
|
-
if (entry.sha256) {
|
|
1525
|
-
const expected = entry.sha256.toLowerCase();
|
|
1526
|
-
const actual = sha256File(tmpFile).toLowerCase();
|
|
1527
|
-
if (actual !== expected) {
|
|
1528
|
-
console.error(chalk.red('Release checksum verification failed'));
|
|
1529
|
-
console.error(chalk.red(`Expected: ${expected}`));
|
|
1530
|
-
console.error(chalk.red(`Actual: ${actual}`));
|
|
1531
|
-
process.exitCode = 1;
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
console.log(chalk.green('Checksum verification passed'));
|
|
1535
|
-
}
|
|
1536
|
-
else {
|
|
1537
|
-
console.log(chalk.yellow.bold('WARNING: manifest entry has no sha256.'));
|
|
1538
|
-
console.log(chalk.yellow(' The release artifact will be installed without integrity verification.'));
|
|
1539
|
-
console.log(chalk.gray(' Set --manifest to a trusted source, or supply sha256 in the manifest entry.'));
|
|
1540
|
-
}
|
|
1541
|
-
console.log(chalk.cyan('Installing update...'));
|
|
1542
|
-
await installGlobalPackageWithNpm(tmpFile);
|
|
1543
|
-
console.log(chalk.green(`Updated to version ${entry.version}`));
|
|
1544
|
-
console.log(chalk.gray('Please restart the CLI to use the new version'));
|
|
1545
|
-
return;
|
|
1546
|
-
}
|
|
1547
|
-
finally {
|
|
1548
|
-
if (fs.existsSync(tmpFile)) {
|
|
1549
|
-
try {
|
|
1550
|
-
fs.unlinkSync(tmpFile);
|
|
1551
|
-
}
|
|
1552
|
-
catch { }
|
|
1553
|
-
}
|
|
1557
|
+
manifestEntry = resolveManifestEntry(response.data || {}, channel);
|
|
1558
|
+
if (!manifestEntry?.version || !manifestEntry?.url) {
|
|
1559
|
+
console.log(chalk.yellow(`Manifest has no installable entry for channel ${channel}; will check npm.`));
|
|
1560
|
+
manifestEntry = null;
|
|
1554
1561
|
}
|
|
1555
1562
|
}
|
|
1556
1563
|
catch (error) {
|
|
1557
|
-
console.
|
|
1558
|
-
console.log(chalk.gray('
|
|
1564
|
+
console.log(chalk.yellow(`Manifest check failed: ${error.message}`));
|
|
1565
|
+
console.log(chalk.gray('Continuing with npm registry check...'));
|
|
1559
1566
|
}
|
|
1560
1567
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1568
|
+
console.log(chalk.cyan('Checking npm registry...'));
|
|
1569
|
+
const npmVersion = await fetchNpmLatestVersion();
|
|
1570
|
+
if (!npmVersion) {
|
|
1571
|
+
console.log(chalk.yellow('Could not read latest version from npm registry.'));
|
|
1572
|
+
}
|
|
1573
|
+
const effectiveLatest = maxVersion(manifestEntry?.version, npmVersion);
|
|
1574
|
+
if (!effectiveLatest) {
|
|
1575
|
+
console.error(chalk.red('Unable to determine the latest CLI version from manifest or npm.'));
|
|
1576
|
+
process.exitCode = 1;
|
|
1563
1577
|
return;
|
|
1564
1578
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1579
|
+
if (manifestEntry?.version
|
|
1580
|
+
&& npmVersion
|
|
1581
|
+
&& compareVersions(npmVersion, manifestEntry.version) > 0) {
|
|
1582
|
+
console.log(chalk.yellow(`Release manifest (${manifestEntry.version}) is behind npm (${npmVersion}); npm will be used when needed.`));
|
|
1583
|
+
}
|
|
1584
|
+
if (!allowDowngrade && compareVersions(effectiveLatest, currentVersion) <= 0) {
|
|
1585
|
+
console.log(chalk.green(`You are running the latest version (${currentVersion})`));
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
console.log(chalk.yellow(`Update available: ${currentVersion} -> ${effectiveLatest}`));
|
|
1589
|
+
if (manifestEntry?.notes && compareVersions(manifestEntry.version, effectiveLatest) >= 0) {
|
|
1590
|
+
console.log(chalk.gray(` ${manifestEntry.notes}`));
|
|
1591
|
+
}
|
|
1592
|
+
if (options.check) {
|
|
1593
|
+
console.log(chalk.gray('Run `vigthoria update` to install the update'));
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
const manifestIsAuthoritative = Boolean(manifestEntry
|
|
1597
|
+
&& manifestEntry.url
|
|
1598
|
+
&& compareVersions(manifestEntry.version, effectiveLatest) >= 0
|
|
1599
|
+
&& compareVersions(manifestEntry.version, currentVersion) > 0);
|
|
1600
|
+
if (manifestIsAuthoritative && manifestEntry) {
|
|
1601
|
+
const tmpFile = path.join(os.tmpdir(), `vigthoria-update-${Date.now()}.tgz`);
|
|
1602
|
+
try {
|
|
1603
|
+
console.log(chalk.cyan(`Downloading release package (${manifestEntry.version})...`));
|
|
1604
|
+
await downloadFile(manifestEntry.url, tmpFile);
|
|
1605
|
+
if (manifestEntry.sha256) {
|
|
1606
|
+
const expected = manifestEntry.sha256.toLowerCase();
|
|
1607
|
+
const actual = sha256File(tmpFile).toLowerCase();
|
|
1608
|
+
if (actual !== expected) {
|
|
1609
|
+
console.error(chalk.red('Release checksum verification failed'));
|
|
1610
|
+
console.error(chalk.red(`Expected: ${expected}`));
|
|
1611
|
+
console.error(chalk.red(`Actual: ${actual}`));
|
|
1612
|
+
process.exitCode = 1;
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
console.log(chalk.green('Checksum verification passed'));
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
console.log(chalk.yellow.bold('WARNING: manifest entry has no sha256.'));
|
|
1619
|
+
console.log(chalk.yellow(' The release artifact will be installed without integrity verification.'));
|
|
1620
|
+
}
|
|
1621
|
+
console.log(chalk.cyan('Installing update...'));
|
|
1622
|
+
await installGlobalPackageWithNpm(tmpFile);
|
|
1623
|
+
console.log(chalk.green(`Updated to version ${manifestEntry.version}`));
|
|
1624
|
+
console.log(chalk.gray('Please restart the CLI to use the new version'));
|
|
1578
1625
|
return;
|
|
1579
1626
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1627
|
+
finally {
|
|
1628
|
+
if (fs.existsSync(tmpFile)) {
|
|
1629
|
+
try {
|
|
1630
|
+
fs.unlinkSync(tmpFile);
|
|
1631
|
+
}
|
|
1632
|
+
catch { }
|
|
1633
|
+
}
|
|
1584
1634
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1635
|
+
}
|
|
1636
|
+
const npmSpec = npmVersion && compareVersions(npmVersion, currentVersion) > 0
|
|
1637
|
+
? `vigthoria-cli@${npmVersion}`
|
|
1638
|
+
: 'vigthoria-cli@latest';
|
|
1639
|
+
try {
|
|
1640
|
+
console.log(chalk.cyan(`Installing update from npm registry (${npmSpec})...`));
|
|
1641
|
+
await installGlobalPackageWithNpm(npmSpec);
|
|
1642
|
+
console.log(chalk.green(`Updated to version ${effectiveLatest}`));
|
|
1588
1643
|
console.log(chalk.gray('Please restart the CLI to use the new version'));
|
|
1589
1644
|
}
|
|
1590
1645
|
catch (error) {
|
|
1591
|
-
console.error(chalk.red('Failed to
|
|
1592
|
-
if (options.check) {
|
|
1593
|
-
console.log(chalk.gray(`npm registry check failed; fallback install target is ${gitPackageSpec}`));
|
|
1594
|
-
return;
|
|
1595
|
-
}
|
|
1646
|
+
console.error(chalk.red('Failed to install via npm registry:'), error.message);
|
|
1596
1647
|
try {
|
|
1597
1648
|
console.log(chalk.cyan('Attempting git package fallback...'));
|
|
1598
1649
|
await installGlobalPackageWithNpm(gitPackageSpec);
|
|
@@ -1601,7 +1652,7 @@ Examples:
|
|
|
1601
1652
|
}
|
|
1602
1653
|
catch (fallbackError) {
|
|
1603
1654
|
console.error(chalk.red('Fallback update also failed:'), fallbackError.message);
|
|
1604
|
-
console.log(chalk.gray(
|
|
1655
|
+
console.log(chalk.gray(`Try manually: npm install -g ${npmSpec}`));
|
|
1605
1656
|
console.log(chalk.gray(`Or: npm install -g ${gitPackageSpec}`));
|
|
1606
1657
|
process.exitCode = 1;
|
|
1607
1658
|
}
|
|
@@ -14,6 +14,10 @@ export interface LiveOutcome {
|
|
|
14
14
|
failedTaskIds: Set<string>;
|
|
15
15
|
completedTaskIds: Set<string>;
|
|
16
16
|
analysisToolsUsed: number;
|
|
17
|
+
/** read_file / grep — not list_directory-only browsing */
|
|
18
|
+
analysisReadToolsUsed: number;
|
|
19
|
+
/** Final user-facing answer text (stream + response body) */
|
|
20
|
+
answerContent?: string;
|
|
17
21
|
streamAborted?: boolean;
|
|
18
22
|
}
|
|
19
23
|
export interface TaskSummaryEvent {
|
|
@@ -27,6 +31,10 @@ export interface RunEvaluation {
|
|
|
27
31
|
uiTheme: 'success' | 'warning' | 'error';
|
|
28
32
|
}
|
|
29
33
|
export declare function createLiveOutcome(): LiveOutcome;
|
|
34
|
+
/** True when text looks like a real answer, not an executor/system placeholder. */
|
|
35
|
+
export declare function isSubstantiveAgentAnswer(text: string): boolean;
|
|
36
|
+
/** Pick the first substantive answer from response body and/or streamed text. */
|
|
37
|
+
export declare function normalizeAgentAnswerContent(...sources: Array<string | undefined | null>): string;
|
|
30
38
|
/** Process executor_complete / task summary events from the V3 SSE stream. */
|
|
31
39
|
export declare function handleTaskEvent(summary: TaskSummaryEvent, liveOutcome: LiveOutcome): void;
|
|
32
40
|
/** Track read/discovery tool usage for analysis-turn accountability. */
|
|
@@ -15,9 +15,38 @@ export function createLiveOutcome() {
|
|
|
15
15
|
failedTaskIds: new Set(),
|
|
16
16
|
completedTaskIds: new Set(),
|
|
17
17
|
analysisToolsUsed: 0,
|
|
18
|
+
analysisReadToolsUsed: 0,
|
|
19
|
+
answerContent: '',
|
|
18
20
|
streamAborted: false,
|
|
19
21
|
};
|
|
20
22
|
}
|
|
23
|
+
const EXECUTOR_PLACEHOLDER_RE = /read-only analysis turn|run at least one discovery tool|reviewing attached files and workspace findings/i;
|
|
24
|
+
const LIST_ONLY_DISCOVERY_TOOLS = new Set([
|
|
25
|
+
'list_directory',
|
|
26
|
+
'list_dir',
|
|
27
|
+
'glob',
|
|
28
|
+
'dir',
|
|
29
|
+
]);
|
|
30
|
+
/** True when text looks like a real answer, not an executor/system placeholder. */
|
|
31
|
+
export function isSubstantiveAgentAnswer(text) {
|
|
32
|
+
const trimmed = String(text || '').trim();
|
|
33
|
+
if (trimmed.length < 30)
|
|
34
|
+
return false;
|
|
35
|
+
if (EXECUTOR_PLACEHOLDER_RE.test(trimmed))
|
|
36
|
+
return false;
|
|
37
|
+
if (/^v3 agent workflow completed\.?$/i.test(trimmed))
|
|
38
|
+
return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/** Pick the first substantive answer from response body and/or streamed text. */
|
|
42
|
+
export function normalizeAgentAnswerContent(...sources) {
|
|
43
|
+
for (const source of sources) {
|
|
44
|
+
const text = String(source || '').trim();
|
|
45
|
+
if (isSubstantiveAgentAnswer(text))
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
21
50
|
/** Process executor_complete / task summary events from the V3 SSE stream. */
|
|
22
51
|
export function handleTaskEvent(summary, liveOutcome) {
|
|
23
52
|
const tid = summary.task_id || summary.id;
|
|
@@ -60,6 +89,9 @@ export function noteAnalysisToolUse(toolName, liveOutcome) {
|
|
|
60
89
|
const normalized = String(toolName || '').trim().toLowerCase();
|
|
61
90
|
if (READ_DISCOVERY_TOOLS.has(normalized)) {
|
|
62
91
|
liveOutcome.analysisToolsUsed += 1;
|
|
92
|
+
if (!LIST_ONLY_DISCOVERY_TOOLS.has(normalized)) {
|
|
93
|
+
liveOutcome.analysisReadToolsUsed += 1;
|
|
94
|
+
}
|
|
63
95
|
}
|
|
64
96
|
}
|
|
65
97
|
/**
|
|
@@ -114,21 +146,37 @@ export function evaluateExecutorSuccess(liveOutcome) {
|
|
|
114
146
|
}
|
|
115
147
|
// Analysis / verification turns with no planned task graph
|
|
116
148
|
if (!liveOutcome.requiresWorkspaceChanges) {
|
|
117
|
-
const
|
|
118
|
-
|
|
149
|
+
const answerText = normalizeAgentAnswerContent(liveOutcome.answerContent);
|
|
150
|
+
const hasSubstantiveAnswer = isSubstantiveAgentAnswer(answerText);
|
|
151
|
+
const hasDeepRead = liveOutcome.analysisReadToolsUsed > 0;
|
|
152
|
+
if (hasFatalError && !hasSubstantiveAnswer) {
|
|
119
153
|
return {
|
|
120
154
|
executorSucceeded: false,
|
|
121
|
-
statusHeadline: '✕ Analysis failed — no
|
|
155
|
+
statusHeadline: '✕ Analysis failed — no answer produced',
|
|
122
156
|
uiTheme: 'error',
|
|
123
157
|
};
|
|
124
158
|
}
|
|
125
|
-
if (!
|
|
159
|
+
if (!hasSubstantiveAnswer) {
|
|
160
|
+
if (liveOutcome.analysisToolsUsed > 0) {
|
|
161
|
+
return {
|
|
162
|
+
executorSucceeded: false,
|
|
163
|
+
statusHeadline: '⚠ Analysis browsed the workspace but did not answer your question — try /retry',
|
|
164
|
+
uiTheme: 'warning',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
126
167
|
return {
|
|
127
168
|
executorSucceeded: false,
|
|
128
169
|
statusHeadline: '⚠ Warning: No read tools confirmed — answer may be incomplete',
|
|
129
170
|
uiTheme: 'warning',
|
|
130
171
|
};
|
|
131
172
|
}
|
|
173
|
+
if (!hasDeepRead) {
|
|
174
|
+
return {
|
|
175
|
+
executorSucceeded: false,
|
|
176
|
+
statusHeadline: '⚠ Analysis answer was not verified against source files — try /retry',
|
|
177
|
+
uiTheme: 'warning',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
132
180
|
return {
|
|
133
181
|
executorSucceeded: true,
|
|
134
182
|
statusHeadline: '✓ Analysis completed',
|
package/dist/utils/api.js
CHANGED
|
@@ -11,6 +11,7 @@ import path from 'path';
|
|
|
11
11
|
import WebSocket from 'ws';
|
|
12
12
|
import { buildSemanticContext } from './context-ranker.js';
|
|
13
13
|
import { getChangedFiles } from './workspace-cache.js';
|
|
14
|
+
import { isSubstantiveAgentAnswer } from './agentRunOutcome.js';
|
|
14
15
|
export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
|
|
15
16
|
export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
|
|
16
17
|
export class CLIError extends Error {
|
|
@@ -2744,9 +2745,10 @@ menu {
|
|
|
2744
2745
|
.map((entry) => `${entry.isDirectory() ? '[DIR] ' : ''}${entry.name}`);
|
|
2745
2746
|
return { success: true, output: entries.join('\n') || '(empty)' };
|
|
2746
2747
|
}
|
|
2747
|
-
if (name === 'glob' || name === 'search_files') {
|
|
2748
|
+
if (name === 'glob' || name === 'search_files' || name === 'grep') {
|
|
2748
2749
|
const pattern = String(args.pattern || args.query || '').toLowerCase();
|
|
2749
2750
|
const files = [];
|
|
2751
|
+
const matches = [];
|
|
2750
2752
|
const walk = (dir) => {
|
|
2751
2753
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2752
2754
|
if (['node_modules', '.git'].includes(entry.name))
|
|
@@ -2757,18 +2759,30 @@ menu {
|
|
|
2757
2759
|
continue;
|
|
2758
2760
|
}
|
|
2759
2761
|
const relative = path.relative(rootPath, absolute).replace(/\\/g, '/');
|
|
2760
|
-
if (
|
|
2761
|
-
|
|
2762
|
-
}
|
|
2763
|
-
else if (name === 'search_files') {
|
|
2764
|
-
const content = fs.readFileSync(absolute, 'utf8');
|
|
2765
|
-
if (content.toLowerCase().includes(pattern))
|
|
2762
|
+
if (name === 'glob' && pattern) {
|
|
2763
|
+
if (!pattern || relative.toLowerCase().includes(pattern.replace(/\*/g, ''))) {
|
|
2766
2764
|
files.push(relative);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
else if (pattern) {
|
|
2768
|
+
try {
|
|
2769
|
+
const content = fs.readFileSync(absolute, 'utf8');
|
|
2770
|
+
if (relative.toLowerCase().includes(pattern.replace(/\*/g, '')) || content.toLowerCase().includes(pattern)) {
|
|
2771
|
+
matches.push(`${relative}: ${content.split('\n').find((line) => line.toLowerCase().includes(pattern))?.trim() || '(match)'}`);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
catch {
|
|
2775
|
+
// skip unreadable/binary files
|
|
2776
|
+
}
|
|
2767
2777
|
}
|
|
2768
2778
|
}
|
|
2769
2779
|
};
|
|
2770
|
-
|
|
2771
|
-
|
|
2780
|
+
const searchRoot = fs.statSync(target.absolutePath).isDirectory() ? target.absolutePath : rootPath;
|
|
2781
|
+
walk(searchRoot);
|
|
2782
|
+
if (name === 'glob') {
|
|
2783
|
+
return { success: true, output: files.slice(0, 200).join('\n') || '(no matches)' };
|
|
2784
|
+
}
|
|
2785
|
+
return { success: true, output: matches.slice(0, 100).join('\n') || '(no matches)' };
|
|
2772
2786
|
}
|
|
2773
2787
|
if (name === 'syntax_check') {
|
|
2774
2788
|
const content = fs.readFileSync(target.absolutePath, 'utf8');
|
|
@@ -2804,8 +2818,27 @@ menu {
|
|
|
2804
2818
|
if (!contextId || !callId)
|
|
2805
2819
|
return;
|
|
2806
2820
|
const backendUrl = responseUrl ? new URL(responseUrl).origin : (context.mcpContextBackendUrl || null);
|
|
2821
|
+
const emitStream = (streamEvent) => {
|
|
2822
|
+
if (typeof context.onStreamEvent === 'function') {
|
|
2823
|
+
try {
|
|
2824
|
+
context.onStreamEvent(streamEvent);
|
|
2825
|
+
}
|
|
2826
|
+
catch {
|
|
2827
|
+
// UI callbacks must never break the client tool bridge.
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
};
|
|
2831
|
+
emitStream({ type: 'tool_call', name: toolName, tool: toolName, arguments: event.arguments || {} });
|
|
2807
2832
|
try {
|
|
2808
2833
|
const result = await this.executeV3ClientToolRequest(event, context);
|
|
2834
|
+
emitStream({
|
|
2835
|
+
type: 'tool_result',
|
|
2836
|
+
name: toolName,
|
|
2837
|
+
tool: toolName,
|
|
2838
|
+
success: result.success,
|
|
2839
|
+
output: result.output,
|
|
2840
|
+
error: result.error || '',
|
|
2841
|
+
});
|
|
2809
2842
|
try {
|
|
2810
2843
|
await this.submitClientToolResult(contextId, callId, result, backendUrl);
|
|
2811
2844
|
}
|
|
@@ -3308,13 +3341,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3308
3341
|
formatV3AgentResponse(data) {
|
|
3309
3342
|
const result = data?.result || {};
|
|
3310
3343
|
if (typeof result === 'string') {
|
|
3311
|
-
|
|
3344
|
+
const text = this.sanitizeV3AgentResponseText(result);
|
|
3345
|
+
if (isSubstantiveAgentAnswer(text))
|
|
3346
|
+
return text;
|
|
3312
3347
|
}
|
|
3313
3348
|
if (typeof result?.summary === 'string' && result.summary.trim()) {
|
|
3314
|
-
|
|
3349
|
+
const text = this.sanitizeV3AgentResponseText(result.summary);
|
|
3350
|
+
if (isSubstantiveAgentAnswer(text))
|
|
3351
|
+
return text;
|
|
3315
3352
|
}
|
|
3316
3353
|
if (typeof result?.message === 'string' && result.message.trim()) {
|
|
3317
|
-
|
|
3354
|
+
const text = this.sanitizeV3AgentResponseText(result.message);
|
|
3355
|
+
if (isSubstantiveAgentAnswer(text))
|
|
3356
|
+
return text;
|
|
3318
3357
|
}
|
|
3319
3358
|
if (Array.isArray(data?.events)) {
|
|
3320
3359
|
const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string' && event.summary.trim());
|
|
@@ -3639,6 +3678,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3639
3678
|
// Exit stream early on complete — agent is done; server-side teardown
|
|
3640
3679
|
// can hold the connection open for many seconds otherwise.
|
|
3641
3680
|
if (event.type === 'complete') {
|
|
3681
|
+
await Promise.allSettled([...this.pendingV3ClientToolTasks]);
|
|
3642
3682
|
reader.cancel().catch(() => { });
|
|
3643
3683
|
return {
|
|
3644
3684
|
task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
|