vigthoria-cli 1.10.0 → 1.10.36
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/auth.d.ts +1 -0
- package/dist/commands/auth.js +99 -17
- package/dist/commands/chat.d.ts +34 -0
- package/dist/commands/chat.js +1162 -61
- package/dist/commands/config.js +11 -2
- package/dist/commands/legion.js +8 -2
- package/dist/commands/wallet.d.ts +25 -0
- package/dist/commands/wallet.js +191 -0
- package/dist/index.js +158 -2
- package/dist/utils/api.d.ts +90 -2
- package/dist/utils/api.js +1730 -266
- package/dist/utils/config.d.ts +11 -0
- package/dist/utils/config.js +53 -12
- package/dist/utils/persona.d.ts +4 -0
- package/dist/utils/persona.js +34 -0
- package/dist/utils/tools.d.ts +5 -0
- package/dist/utils/tools.js +53 -1
- package/dist/utils/workspace-stream.js +56 -15
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +4 -2
- package/scripts/release/validate-no-go-gates.sh +3 -1
package/dist/commands/chat.js
CHANGED
|
@@ -4,13 +4,14 @@ import * as os from 'os';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as readline from 'readline';
|
|
6
6
|
import { createSpinner } from '../utils/logger.js';
|
|
7
|
-
import { APIClient, CLIError, classifyError, formatCLIError, sanitizeUserFacingErrorText, sanitizeUserFacingPathText, propagateError } from '../utils/api.js';
|
|
7
|
+
import { APIClient, CLIError, classifyError, formatCLIError, sanitizeUserFacingErrorText, sanitizeUserFacingPathText, propagateError, VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE } from '../utils/api.js';
|
|
8
8
|
import { AgenticTools, robustifyStreamResponse } from '../utils/tools.js';
|
|
9
9
|
import { SessionManager } from '../utils/session.js';
|
|
10
10
|
import { BridgeClient, getBridgeClient } from '../utils/bridge-client.js';
|
|
11
|
-
import { WorkspaceWatcher } from '../utils/workspace-stream.js';
|
|
11
|
+
import { WorkspaceWatcher, WorkspaceWSClient } from '../utils/workspace-stream.js';
|
|
12
12
|
import { TaskDisplay } from '../utils/task-display.js';
|
|
13
13
|
import { ProjectMemoryService } from '../utils/project-memory.js';
|
|
14
|
+
import { buildPersonaOverlay, normalizePersonaMode } from '../utils/persona.js';
|
|
14
15
|
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
15
16
|
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
|
|
16
17
|
if (!rawValue) {
|
|
@@ -22,18 +23,18 @@ const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
|
22
23
|
const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
23
24
|
const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS;
|
|
24
25
|
if (!rawValue) {
|
|
25
|
-
return
|
|
26
|
+
return 90000;
|
|
26
27
|
}
|
|
27
28
|
const parsed = Number.parseInt(rawValue, 10);
|
|
28
|
-
return Number.isFinite(parsed) && parsed
|
|
29
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
|
|
29
30
|
})();
|
|
30
31
|
const DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS = (() => {
|
|
31
32
|
const rawValue = process.env.VIGTHORIA_AGENT_SOFT_TIMEOUT_MS || process.env.V3_AGENT_SOFT_TIMEOUT_MS;
|
|
32
33
|
if (!rawValue) {
|
|
33
|
-
return
|
|
34
|
+
return 180000;
|
|
34
35
|
}
|
|
35
36
|
const parsed = Number.parseInt(rawValue, 10);
|
|
36
|
-
return Number.isFinite(parsed) && parsed
|
|
37
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 180000;
|
|
37
38
|
})();
|
|
38
39
|
export class ChatCommand {
|
|
39
40
|
config;
|
|
@@ -52,6 +53,7 @@ export class ChatCommand {
|
|
|
52
53
|
currentModel = 'code';
|
|
53
54
|
modelExplicitlySelected = false;
|
|
54
55
|
autoApprove = false;
|
|
56
|
+
personaOverride = null;
|
|
55
57
|
// Phase 5: Agent quality gate — track tool usage for evidence thresholds
|
|
56
58
|
agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
|
|
57
59
|
operatorMode = false;
|
|
@@ -112,17 +114,22 @@ export class ChatCommand {
|
|
|
112
114
|
toUserFacingApiError(error, context) {
|
|
113
115
|
const classified = classifyError(error);
|
|
114
116
|
const status = classified.statusCode || (this.isJwtExpirationError(error) ? 401 : 500);
|
|
115
|
-
if (this.isJwtExpirationError(error)) {
|
|
117
|
+
if (this.isJwtExpirationError(error) || classified.category === 'auth') {
|
|
116
118
|
return new CLIError('Your Vigthoria session has expired. Run `vigthoria login` to authenticate again.', 'auth', { statusCode: 401 });
|
|
117
119
|
}
|
|
118
|
-
|
|
120
|
+
// Preserve structured API classification first (auth/model/network/etc.)
|
|
121
|
+
// so upstream responses are not relabeled by message heuristics.
|
|
122
|
+
if (classified.category === 'timeout') {
|
|
119
123
|
return new CLIError(`${context} timed out. Check your connection and try again.`, 'timeout', { statusCode: status });
|
|
120
124
|
}
|
|
121
|
-
if (
|
|
125
|
+
if (classified.category === 'network') {
|
|
122
126
|
return new CLIError(`${context} could not reach the Vigthoria API. Check your network connection and try again.`, 'network', { statusCode: status });
|
|
123
127
|
}
|
|
128
|
+
if (classified.category === 'model_backend') {
|
|
129
|
+
return new CLIError(VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE, 'model_backend', { statusCode: status, endpoint: classified.endpoint });
|
|
130
|
+
}
|
|
124
131
|
const message = sanitizeUserFacingErrorText(classified.message || `${context} failed`);
|
|
125
|
-
return new CLIError(message,
|
|
132
|
+
return new CLIError(message, 'model_backend', { statusCode: status, endpoint: classified.endpoint });
|
|
126
133
|
}
|
|
127
134
|
handleApiError(error, context) {
|
|
128
135
|
const userFacingError = this.toUserFacingApiError(error, context);
|
|
@@ -151,10 +158,6 @@ export class ChatCommand {
|
|
|
151
158
|
return await operation();
|
|
152
159
|
}
|
|
153
160
|
catch (error) {
|
|
154
|
-
if (!this.jsonOutput) {
|
|
155
|
-
const transient = this.isTimeoutError(error) ? 'timeout' : this.isNetworkError(error) ? 'network error' : 'API error';
|
|
156
|
-
console.error(chalk.red(`${context} failed with ${transient}: ${this.toUserFacingApiError(error, context).message}`));
|
|
157
|
-
}
|
|
158
161
|
lastError = error;
|
|
159
162
|
if (this.isJwtExpirationError(error)) {
|
|
160
163
|
this.handleApiError(error, context);
|
|
@@ -163,7 +166,8 @@ export class ChatCommand {
|
|
|
163
166
|
this.handleApiError(error, context);
|
|
164
167
|
}
|
|
165
168
|
if (!this.jsonOutput) {
|
|
166
|
-
|
|
169
|
+
const transient = this.isTimeoutError(error) ? 'timeout' : this.isNetworkError(error) ? 'network error' : 'temporary service issue';
|
|
170
|
+
this.logger.warn(`${context} failed due to ${transient}; retrying (${attempt + 1}/${retries})...`);
|
|
167
171
|
}
|
|
168
172
|
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
|
|
169
173
|
}
|
|
@@ -262,7 +266,7 @@ export class ChatCommand {
|
|
|
262
266
|
explicitModel: true,
|
|
263
267
|
heavyTask,
|
|
264
268
|
cloudEligible,
|
|
265
|
-
cloudSelected:
|
|
269
|
+
cloudSelected: ['cloud', 'cloud-reason', 'ultra', 'cloud-fast', 'cloud-balanced', 'cloud-code', 'cloud-power', 'cloud-maximum'].includes(this.currentModel),
|
|
266
270
|
routeReason: 'explicit-model-selection',
|
|
267
271
|
};
|
|
268
272
|
}
|
|
@@ -295,8 +299,19 @@ export class ChatCommand {
|
|
|
295
299
|
routeReason: 'default-v3-agent',
|
|
296
300
|
};
|
|
297
301
|
}
|
|
298
|
-
getMessagesForModel() {
|
|
302
|
+
getMessagesForModel(options) {
|
|
299
303
|
const messages = [...this.messages];
|
|
304
|
+
const personaOverlay = this.buildActivePersonaOverlay();
|
|
305
|
+
if (personaOverlay && !messages.some((message) => message.role === 'system' && message.content.includes('Optional persona overlay: Wiener Grantler mode.'))) {
|
|
306
|
+
const insertionIndex = messages.findIndex((message) => message.role !== 'system');
|
|
307
|
+
const personaMessage = { role: 'system', content: personaOverlay };
|
|
308
|
+
if (insertionIndex === -1) {
|
|
309
|
+
messages.push(personaMessage);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
messages.splice(insertionIndex, 0, personaMessage);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
300
315
|
const memoryContexts = [
|
|
301
316
|
this.sessionManager.buildMemoryContext(this.currentSession),
|
|
302
317
|
this.projectMemory?.buildContextForPrompt(this.getLastUserPrompt()) || '',
|
|
@@ -318,8 +333,137 @@ export class ChatCommand {
|
|
|
318
333
|
messages.splice(insertionIndex, 0, memoryMessage);
|
|
319
334
|
}
|
|
320
335
|
}
|
|
336
|
+
if (options?.compact) {
|
|
337
|
+
const compactLimit = 6000;
|
|
338
|
+
const systemMessages = messages.filter((message) => message.role === 'system').map((message) => ({
|
|
339
|
+
...message,
|
|
340
|
+
content: message.content.length > compactLimit
|
|
341
|
+
? `${message.content.slice(0, compactLimit)}\n...[trimmed for first local agent turn]`
|
|
342
|
+
: message.content,
|
|
343
|
+
}));
|
|
344
|
+
const lastUser = [...messages].reverse().find((message) => message.role === 'user');
|
|
345
|
+
return lastUser ? [...systemMessages.slice(0, 2), lastUser] : systemMessages.slice(0, 2);
|
|
346
|
+
}
|
|
321
347
|
return messages;
|
|
322
348
|
}
|
|
349
|
+
normalizeClientV3ToolPath(rawPath) {
|
|
350
|
+
let normalized = String(rawPath || '.').trim().replace(/\\/g, '/');
|
|
351
|
+
if (!normalized || normalized === '/') {
|
|
352
|
+
return '.';
|
|
353
|
+
}
|
|
354
|
+
normalized = normalized.replace(/^vigthoria:\/\/workspace\/?/i, '');
|
|
355
|
+
normalized = normalized.replace(/^\.\/+/, '');
|
|
356
|
+
normalized = normalized.replace(/^workspace\/?/i, '');
|
|
357
|
+
const workspaceRoot = this.currentProjectPath || process.cwd();
|
|
358
|
+
const workspaceName = path.basename(workspaceRoot);
|
|
359
|
+
if (workspaceName) {
|
|
360
|
+
const workspaceKey = workspaceName.toLowerCase().replace(/[\s_./\\-]+/g, '');
|
|
361
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
362
|
+
if (parts.length > 0) {
|
|
363
|
+
const firstKey = parts[0].toLowerCase().replace(/[\s_./\\-]+/g, '');
|
|
364
|
+
if (firstKey === workspaceKey) {
|
|
365
|
+
normalized = parts.slice(1).join('/') || '.';
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
normalized = normalized.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
370
|
+
return normalized || '.';
|
|
371
|
+
}
|
|
372
|
+
resolveClientV3ToolPath(rawPath) {
|
|
373
|
+
const root = this.currentProjectPath || process.cwd();
|
|
374
|
+
const normalized = this.normalizeClientV3ToolPath(rawPath);
|
|
375
|
+
const absoluteTarget = path.resolve(root, normalized === '.' ? '' : normalized);
|
|
376
|
+
if (fs.existsSync(absoluteTarget)) {
|
|
377
|
+
return normalized;
|
|
378
|
+
}
|
|
379
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
380
|
+
if (parts.length === 0) {
|
|
381
|
+
return '.';
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
385
|
+
const firstKey = parts[0].toLowerCase().replace(/[\s_./\\-]+/g, '');
|
|
386
|
+
for (const entry of entries) {
|
|
387
|
+
const entryKey = entry.name.toLowerCase().replace(/[\s_./\\-]+/g, '');
|
|
388
|
+
if (entryKey === firstKey || entryKey.includes(firstKey) || firstKey.includes(entryKey)) {
|
|
389
|
+
const rest = parts.slice(1).join('/');
|
|
390
|
+
const candidate = rest ? `${entry.name}/${rest}` : entry.name;
|
|
391
|
+
if (fs.existsSync(path.resolve(root, candidate))) {
|
|
392
|
+
return candidate.replace(/\\/g, '/');
|
|
393
|
+
}
|
|
394
|
+
if (!rest) {
|
|
395
|
+
return entry.name.replace(/\\/g, '/');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// Ignore unreadable workspace roots during path repair.
|
|
402
|
+
}
|
|
403
|
+
return normalized;
|
|
404
|
+
}
|
|
405
|
+
normalizeClientV3ToolArgs(args) {
|
|
406
|
+
const normalizedArgs = { ...args };
|
|
407
|
+
for (const key of ['path', 'file_path', 'file', 'target']) {
|
|
408
|
+
if (typeof normalizedArgs[key] === 'string' && normalizedArgs[key].trim()) {
|
|
409
|
+
normalizedArgs[key] = this.resolveClientV3ToolPath(normalizedArgs[key]);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return normalizedArgs;
|
|
413
|
+
}
|
|
414
|
+
async executeClientV3Tool(event) {
|
|
415
|
+
if (!this.tools) {
|
|
416
|
+
return { success: false, output: '', error: 'Local agent tools are not initialized.' };
|
|
417
|
+
}
|
|
418
|
+
const name = String(event.name || '').trim();
|
|
419
|
+
const args = (event.arguments && typeof event.arguments === 'object')
|
|
420
|
+
? event.arguments
|
|
421
|
+
: {};
|
|
422
|
+
let toolName = name;
|
|
423
|
+
let toolArgs = {};
|
|
424
|
+
for (const [key, value] of Object.entries(args)) {
|
|
425
|
+
toolArgs[key] = value == null ? '' : String(value);
|
|
426
|
+
}
|
|
427
|
+
toolArgs = this.normalizeClientV3ToolArgs(toolArgs);
|
|
428
|
+
if (name === 'list_directory') {
|
|
429
|
+
toolName = 'list_dir';
|
|
430
|
+
toolArgs = {
|
|
431
|
+
path: toolArgs.path || '.',
|
|
432
|
+
...(toolArgs.recursive === 'true' || toolArgs.recursive === '1' ? { recursive: 'true' } : {}),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
else if (name === 'search_files') {
|
|
436
|
+
toolName = 'grep';
|
|
437
|
+
toolArgs = {
|
|
438
|
+
path: toolArgs.path || '.',
|
|
439
|
+
pattern: toolArgs.pattern || toolArgs.query || '',
|
|
440
|
+
...(toolArgs.file_pattern ? { includePattern: toolArgs.file_pattern } : {}),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
else if (name === 'read_file') {
|
|
444
|
+
toolName = 'read_file';
|
|
445
|
+
}
|
|
446
|
+
else if (name === 'write_file' || name === 'edit_file') {
|
|
447
|
+
toolName = name;
|
|
448
|
+
}
|
|
449
|
+
if (!toolName) {
|
|
450
|
+
return { success: false, output: '', error: 'Missing V3 client tool name.' };
|
|
451
|
+
}
|
|
452
|
+
const result = await this.tools.execute({ tool: toolName, args: toolArgs });
|
|
453
|
+
return {
|
|
454
|
+
success: result.success === true,
|
|
455
|
+
output: String(result.output || result.message || ''),
|
|
456
|
+
error: result.error ? String(result.error) : '',
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
getActivePersonaMode() {
|
|
460
|
+
if (this.personaOverride)
|
|
461
|
+
return this.personaOverride;
|
|
462
|
+
return normalizePersonaMode(this.config.get('persona')) || 'default';
|
|
463
|
+
}
|
|
464
|
+
buildActivePersonaOverlay() {
|
|
465
|
+
return buildPersonaOverlay(this.getActivePersonaMode(), this.getLastUserPrompt());
|
|
466
|
+
}
|
|
323
467
|
isDiagnosticPrompt(prompt) {
|
|
324
468
|
return /(startup|start up|won'?t start|doesn'?t start|crash|crashes|error|errors|failing|fails|issue|issues|bug|bugs|diagnos|debug|runtime|log|logs|exception|traceback|stack trace|yaml|blocking|blocker)/i.test(prompt);
|
|
325
469
|
}
|
|
@@ -328,10 +472,270 @@ export class ChatCommand {
|
|
|
328
472
|
* question — these should use analysis_only workflow, not full_autonomy.
|
|
329
473
|
*/
|
|
330
474
|
isAnalysisLookupPrompt(prompt) {
|
|
475
|
+
if (this.isImplementationPrompt(prompt)) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
331
478
|
return /^(what|which|where|how many|who|find|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|count|read|look at|tell me|locate|search for|does .* exist)/i.test(prompt.trim());
|
|
332
479
|
}
|
|
480
|
+
isImplementationPrompt(prompt) {
|
|
481
|
+
const trimmed = String(prompt || '').trim();
|
|
482
|
+
if (!trimmed)
|
|
483
|
+
return false;
|
|
484
|
+
if (/\b(implement|add|fix|repair|complete|finish|build|create|write|patch|update|modify|edit|make\s+(?:it\s+)?work(?:ing)?|get\s+(?:a\s+)?working)\b/i.test(trimmed)) {
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
if (/\bwhat(?:'s|\s+is|\s+are)\s+missing\b/i.test(trimmed)) {
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
if (/\b(game|html5|pacman|rogue|app|site)\b/i.test(trimmed) && /\b(missing|broken|fix|implement|working|playable)\b/i.test(trimmed)) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
if (/continue the previous agent run/i.test(trimmed) && /\b(implement|fix|missing|blocker|remaining)\b/i.test(trimmed)) {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
getWindowsPromptPathRoots() {
|
|
499
|
+
const roots = new Set();
|
|
500
|
+
const add = (value) => {
|
|
501
|
+
const raw = String(value || '').trim();
|
|
502
|
+
if (!raw)
|
|
503
|
+
return;
|
|
504
|
+
try {
|
|
505
|
+
const resolved = path.resolve(raw);
|
|
506
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
507
|
+
roots.add(resolved);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// Ignore unreadable discovery roots.
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
const home = os.homedir();
|
|
515
|
+
const cwdRoot = path.parse(process.cwd()).root || path.parse(home).root || '';
|
|
516
|
+
add(process.cwd());
|
|
517
|
+
add(home);
|
|
518
|
+
add(path.join(home, 'Desktop'));
|
|
519
|
+
add(path.join(home, 'Documents'));
|
|
520
|
+
add(process.env.USERPROFILE);
|
|
521
|
+
add(process.env.OneDrive);
|
|
522
|
+
add(process.env.OneDriveCommercial);
|
|
523
|
+
add(process.env.OneDriveConsumer);
|
|
524
|
+
if (process.env.OneDrive) {
|
|
525
|
+
add(path.join(process.env.OneDrive, 'Desktop'));
|
|
526
|
+
add(path.join(process.env.OneDrive, 'Documents'));
|
|
527
|
+
}
|
|
528
|
+
if (cwdRoot) {
|
|
529
|
+
add(cwdRoot);
|
|
530
|
+
add(path.join(cwdRoot, 'vigthoria'));
|
|
531
|
+
add(path.join(cwdRoot, 'Vigthoria'));
|
|
532
|
+
}
|
|
533
|
+
return Array.from(roots);
|
|
534
|
+
}
|
|
535
|
+
findPromptDirectoryByName(rawPath) {
|
|
536
|
+
if (os.platform() !== 'win32')
|
|
537
|
+
return null;
|
|
538
|
+
const normalized = String(rawPath || '').trim().replace(/^\/+/, '').replace(/[\\/]+/g, path.sep);
|
|
539
|
+
if (!normalized)
|
|
540
|
+
return null;
|
|
541
|
+
const wantedParts = normalized.split(/[\\/]+/).filter(Boolean);
|
|
542
|
+
const wantedName = wantedParts[wantedParts.length - 1];
|
|
543
|
+
if (!wantedName)
|
|
544
|
+
return null;
|
|
545
|
+
const maxDepth = 3;
|
|
546
|
+
const maxEntries = 2500;
|
|
547
|
+
const roots = this.getWindowsPromptPathRoots();
|
|
548
|
+
for (const root of roots) {
|
|
549
|
+
let visited = 0;
|
|
550
|
+
const direct = path.join(root, normalized);
|
|
551
|
+
try {
|
|
552
|
+
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
|
|
553
|
+
return direct;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
// Continue with bounded search.
|
|
558
|
+
}
|
|
559
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
560
|
+
while (stack.length > 0 && visited < maxEntries) {
|
|
561
|
+
const current = stack.pop();
|
|
562
|
+
if (!current || current.depth > maxDepth)
|
|
563
|
+
continue;
|
|
564
|
+
let entries;
|
|
565
|
+
try {
|
|
566
|
+
entries = fs.readdirSync(current.dir, { withFileTypes: true });
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
visited += entries.length;
|
|
572
|
+
for (const entry of entries) {
|
|
573
|
+
if (!entry.isDirectory())
|
|
574
|
+
continue;
|
|
575
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('$'))
|
|
576
|
+
continue;
|
|
577
|
+
const fullPath = path.join(current.dir, entry.name);
|
|
578
|
+
if (entry.name.toLowerCase() === wantedName.toLowerCase()) {
|
|
579
|
+
if (wantedParts.length === 1 || fullPath.toLowerCase().endsWith(normalized.toLowerCase())) {
|
|
580
|
+
return fullPath;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (current.depth < maxDepth) {
|
|
584
|
+
stack.push({ dir: fullPath, depth: current.depth + 1 });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
extractExplicitLocalPath(prompt) {
|
|
592
|
+
const resolveExistingPath = (rawPath) => {
|
|
593
|
+
const trimmed = String(rawPath || '').trim().replace(/^['"]|['"]$/g, '');
|
|
594
|
+
if (!trimmed)
|
|
595
|
+
return null;
|
|
596
|
+
const candidates = [];
|
|
597
|
+
candidates.push(path.resolve(trimmed));
|
|
598
|
+
if (os.platform() === 'win32' && /^\/[A-Za-z0-9._ -]/.test(trimmed)) {
|
|
599
|
+
const withoutLeadingSlash = trimmed.replace(/^\/+/, '');
|
|
600
|
+
const cwdRoot = path.parse(process.cwd()).root || '';
|
|
601
|
+
// Allow prompts like "/Vigthoria Games" to resolve as "C:/Vigthoria/Games".
|
|
602
|
+
const asSegments = withoutLeadingSlash.replace(/\s+/g, path.sep);
|
|
603
|
+
for (const root of this.getWindowsPromptPathRoots()) {
|
|
604
|
+
candidates.push(path.resolve(root, withoutLeadingSlash));
|
|
605
|
+
candidates.push(path.resolve(root, asSegments));
|
|
606
|
+
const segmentParts = asSegments.split(/[\\/]+/).filter(Boolean);
|
|
607
|
+
if (segmentParts.length > 1 && path.basename(root).toLowerCase() === segmentParts[0].toLowerCase()) {
|
|
608
|
+
candidates.push(path.resolve(root, ...segmentParts.slice(1)));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (cwdRoot && !candidates.includes(path.resolve(cwdRoot, withoutLeadingSlash))) {
|
|
612
|
+
candidates.push(path.resolve(cwdRoot, withoutLeadingSlash));
|
|
613
|
+
candidates.push(path.resolve(cwdRoot, asSegments));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
for (const candidate of candidates) {
|
|
617
|
+
try {
|
|
618
|
+
if (fs.existsSync(candidate)) {
|
|
619
|
+
return candidate;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
// Continue trying other normalized candidates.
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const discovered = this.findPromptDirectoryByName(trimmed);
|
|
627
|
+
if (discovered)
|
|
628
|
+
return discovered;
|
|
629
|
+
return null;
|
|
630
|
+
};
|
|
631
|
+
// Quoted paths first (supports spaces safely).
|
|
632
|
+
const quotedPatterns = [
|
|
633
|
+
/"([A-Za-z]:[\\/][^"\r\n]+)"/,
|
|
634
|
+
/'([A-Za-z]:[\\/][^'\r\n]+)'/,
|
|
635
|
+
/"(\/[^"]+)"/,
|
|
636
|
+
/'(\/[^']+)'/,
|
|
637
|
+
];
|
|
638
|
+
for (const pattern of quotedPatterns) {
|
|
639
|
+
const match = prompt.match(pattern);
|
|
640
|
+
if (match?.[1]) {
|
|
641
|
+
const resolved = resolveExistingPath(match[1]);
|
|
642
|
+
if (resolved)
|
|
643
|
+
return resolved;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// Unquoted Windows path with optional spaces, stopping before instruction connectors.
|
|
647
|
+
const windowsMatch = prompt.match(/(?:^|\s)([A-Za-z]:[\\/][^<>"|?*\r\n]+?)(?=(?:\s+(?:and|then|to|for|with|where|that|which|who|make|create|build|analyse|analyze)\b|\s{2,}|[.,;!?)]|$))/i);
|
|
648
|
+
if (windowsMatch?.[1]) {
|
|
649
|
+
const resolved = resolveExistingPath(windowsMatch[1]);
|
|
650
|
+
if (resolved)
|
|
651
|
+
return resolved;
|
|
652
|
+
}
|
|
653
|
+
// Unix-style absolute path with optional spaces (but not URLs).
|
|
654
|
+
if (!/(https?|ftp):\/\//i.test(prompt)) {
|
|
655
|
+
const unixMatch = prompt.match(/(?:^|\s)(\/[a-zA-Z0-9._\-/ ]+?)(?=(?:\s+(?:and|then|to|for|with|where|that|which|who|make|create|build|analyse|analyze)\b|\s{2,}|[.,;!?)]|$))/i);
|
|
656
|
+
if (unixMatch?.[1]) {
|
|
657
|
+
const candidatePath = unixMatch[1]
|
|
658
|
+
.replace(/\s+(and|or|at|in|the|to|for|with|from|by|on)$/i, '')
|
|
659
|
+
.replace(/[.,;!?:)\]]*$/, '')
|
|
660
|
+
.trim();
|
|
661
|
+
if (candidatePath.length > 1) {
|
|
662
|
+
const resolved = resolveExistingPath(candidatePath);
|
|
663
|
+
if (resolved)
|
|
664
|
+
return resolved;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
isUnscopedPromptPathOverrideAllowed() {
|
|
671
|
+
return /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_ALLOW_UNSCOPED_PROMPT_PATHS || ''));
|
|
672
|
+
}
|
|
673
|
+
isPathWithinRoot(candidatePath, rootPath) {
|
|
674
|
+
const candidate = path.resolve(candidatePath);
|
|
675
|
+
const root = path.resolve(rootPath);
|
|
676
|
+
const rel = path.relative(root, candidate);
|
|
677
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
678
|
+
}
|
|
679
|
+
getPromptPathAllowedRoots(baseWorkspace) {
|
|
680
|
+
const roots = new Set();
|
|
681
|
+
const addRoot = (rawValue) => {
|
|
682
|
+
const value = String(rawValue || '').trim();
|
|
683
|
+
if (!value)
|
|
684
|
+
return;
|
|
685
|
+
const resolved = path.resolve(value);
|
|
686
|
+
try {
|
|
687
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
688
|
+
roots.add(resolved);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// ignore invalid or unreadable roots
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
addRoot(baseWorkspace);
|
|
696
|
+
addRoot(this.currentProjectPath);
|
|
697
|
+
addRoot(process.cwd());
|
|
698
|
+
addRoot(this.config.get('project')?.rootPath || null);
|
|
699
|
+
const envRootsRaw = String(process.env.VIGTHORIA_ALLOWED_WORKSPACE_ROOTS || '').trim();
|
|
700
|
+
if (envRootsRaw) {
|
|
701
|
+
for (const entry of envRootsRaw.split(path.delimiter)) {
|
|
702
|
+
addRoot(entry);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return Array.from(roots);
|
|
706
|
+
}
|
|
707
|
+
resolvePromptWorkspacePath(prompt, baseWorkspace) {
|
|
708
|
+
const explicitPath = this.extractExplicitLocalPath(prompt);
|
|
709
|
+
if (!explicitPath) {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
const candidate = path.resolve(explicitPath);
|
|
713
|
+
if (this.isUnscopedPromptPathOverrideAllowed()) {
|
|
714
|
+
return candidate;
|
|
715
|
+
}
|
|
716
|
+
const runtime = this.getRuntimeEnvironmentContext();
|
|
717
|
+
// On local-machine CLI sessions, allow explicit absolute paths from user prompts.
|
|
718
|
+
// The path still must exist on disk (validated by extractExplicitLocalPath).
|
|
719
|
+
if (runtime.machineScope === 'local-machine') {
|
|
720
|
+
return candidate;
|
|
721
|
+
}
|
|
722
|
+
const allowedRoots = this.getPromptPathAllowedRoots(baseWorkspace);
|
|
723
|
+
const isAllowed = allowedRoots.some((root) => this.isPathWithinRoot(candidate, root));
|
|
724
|
+
if (isAllowed) {
|
|
725
|
+
return candidate;
|
|
726
|
+
}
|
|
727
|
+
if (!this.jsonOutput) {
|
|
728
|
+
console.log(chalk.yellow(`Ignoring path outside allowed workspace roots: ${candidate}`));
|
|
729
|
+
if (allowedRoots.length > 0) {
|
|
730
|
+
const displayRoots = allowedRoots.map((root) => root.replace(/\\/g, '/')).join(', ');
|
|
731
|
+
console.log(chalk.gray(`Allowed roots: ${displayRoots}`));
|
|
732
|
+
}
|
|
733
|
+
console.log(chalk.gray('To allow unrestricted prompt path overrides, set VIGTHORIA_ALLOW_UNSCOPED_PROMPT_PATHS=1.'));
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
333
737
|
isBrowserTaskPrompt(prompt) {
|
|
334
|
-
return /(
|
|
738
|
+
return /(\bbrowser\b|\bchrome\b|\bdevtools\b|\bconsole\b|\bdom\b|network tab|network request|frontend runtime|client-side|client side|rendering|page load|\bwebsocket\b|ui bug|inspect element)/i.test(prompt);
|
|
335
739
|
}
|
|
336
740
|
/**
|
|
337
741
|
* Returns true when a prompt can be answered directly without the full
|
|
@@ -377,13 +781,36 @@ export class ChatCommand {
|
|
|
377
781
|
inferAgentTaskType(prompt) {
|
|
378
782
|
if (this.isDiagnosticPrompt(prompt))
|
|
379
783
|
return 'debugging';
|
|
784
|
+
if (this.isImplementationPrompt(prompt)) {
|
|
785
|
+
return /\b(game|html5|pacman|rogue|playable)\b/i.test(prompt) ? 'game-build' : 'implementation';
|
|
786
|
+
}
|
|
380
787
|
if (/^(what|which|how many|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|find|count|read|look at|tell me)/i.test(prompt.trim()))
|
|
381
788
|
return 'analysis';
|
|
382
789
|
return 'implementation';
|
|
383
790
|
}
|
|
791
|
+
bindPromptWorkspace(prompt) {
|
|
792
|
+
const resolved = this.resolvePromptWorkspacePath(prompt, this.currentProjectPath);
|
|
793
|
+
if (!resolved) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const normalizedResolved = path.resolve(resolved);
|
|
797
|
+
const normalizedCurrent = path.resolve(this.currentProjectPath);
|
|
798
|
+
if (normalizedResolved === normalizedCurrent) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (!this.jsonOutput) {
|
|
802
|
+
console.log(chalk.cyan(`📁 Workspace from prompt: ${normalizedResolved}`));
|
|
803
|
+
}
|
|
804
|
+
this.currentProjectPath = normalizedResolved;
|
|
805
|
+
this.tools?.setWorkspaceRoot(normalizedResolved);
|
|
806
|
+
this.projectMemory = new ProjectMemoryService(normalizedResolved);
|
|
807
|
+
}
|
|
384
808
|
buildTaskShapingInstructions(prompt) {
|
|
385
809
|
const instructions = [];
|
|
386
810
|
const runtime = this.getRuntimeEnvironmentContext();
|
|
811
|
+
if (runtime.machineScope === 'local-machine') {
|
|
812
|
+
instructions.push(`Execution environment: local user machine (${runtime.platform}).`, `Project workspace root on this machine: ${runtime.workspacePath}.`, 'All list_dir, read_file, grep, glob, and bash tools operate on this local filesystem — not a remote server copy.', 'Use paths relative to that root only. Never prefix paths with workspace/ or repeat the workspace folder name.', 'Start with list_dir on "." to discover the real folder structure before reading files.', 'If read_file fails, use list_dir or glob to locate the correct relative path, then retry read_file.');
|
|
813
|
+
}
|
|
387
814
|
// Platform-aware routing hints
|
|
388
815
|
if (runtime.platform === 'windows') {
|
|
389
816
|
instructions.push('Platform: Windows. Use list_dir, glob, read_file, and the grep tool for searching.', 'The grep tool handles Windows automatically — do not use bash to call grep, findstr, or Select-String manually.', 'Do not use bash for Unix commands (cat, head, tail, awk, sed, wc).', 'Use read_file to inspect file contents instead of shell commands.', 'All file paths use forward slashes internally.');
|
|
@@ -495,6 +922,9 @@ export class ChatCommand {
|
|
|
495
922
|
v3ToolCallCount = 0;
|
|
496
923
|
v3LastActivity = Date.now();
|
|
497
924
|
v3StreamingStarted = false;
|
|
925
|
+
v3StreamedTextBuffer = '';
|
|
926
|
+
v3LiveToolEvidence = [];
|
|
927
|
+
v3PendingToolCalls = [];
|
|
498
928
|
/**
|
|
499
929
|
* Strip server-internal path prefixes from tool output strings.
|
|
500
930
|
* Prevents exposing paths like /var/www/V3-Code-Agent/... to end users.
|
|
@@ -502,7 +932,16 @@ export class ChatCommand {
|
|
|
502
932
|
sanitizeServerPath(text) {
|
|
503
933
|
if (!text)
|
|
504
934
|
return text;
|
|
505
|
-
return sanitizeUserFacingPathText(text);
|
|
935
|
+
return sanitizeUserFacingPathText(this.stripHiddenThoughtBlocks(text));
|
|
936
|
+
}
|
|
937
|
+
stripHiddenThoughtBlocks(text) {
|
|
938
|
+
if (!text)
|
|
939
|
+
return text;
|
|
940
|
+
return text
|
|
941
|
+
.replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
|
|
942
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
|
943
|
+
.replace(/<\|(?:mask_start|mask_end)\|>/g, '')
|
|
944
|
+
.trim();
|
|
506
945
|
}
|
|
507
946
|
describeV3AgentTool(toolName) {
|
|
508
947
|
const normalized = String(toolName || '').toLowerCase();
|
|
@@ -560,6 +999,7 @@ export class ChatCommand {
|
|
|
560
999
|
if (!safeText) {
|
|
561
1000
|
return;
|
|
562
1001
|
}
|
|
1002
|
+
this.v3StreamedTextBuffer += safeText;
|
|
563
1003
|
if (!this.v3StreamingStarted) {
|
|
564
1004
|
this.v3StreamingStarted = true;
|
|
565
1005
|
spinner.stop();
|
|
@@ -572,6 +1012,117 @@ export class ChatCommand {
|
|
|
572
1012
|
}
|
|
573
1013
|
process.stdout.write(safeText);
|
|
574
1014
|
}
|
|
1015
|
+
isGenericV3AgentContent(text) {
|
|
1016
|
+
const value = String(text || '').trim();
|
|
1017
|
+
return !value || /^(v3 agent workflow completed\.?|task completed|agent run finished|workflow completed\.?)$/i.test(value);
|
|
1018
|
+
}
|
|
1019
|
+
hasAlreadyStreamedV3Content(text) {
|
|
1020
|
+
const value = String(text || '').trim();
|
|
1021
|
+
if (!value)
|
|
1022
|
+
return true;
|
|
1023
|
+
return this.v3StreamedTextBuffer.includes(value) || value.includes(this.v3StreamedTextBuffer.trim());
|
|
1024
|
+
}
|
|
1025
|
+
isThinV3Summary(text) {
|
|
1026
|
+
const value = String(text || '').trim();
|
|
1027
|
+
if (!value)
|
|
1028
|
+
return true;
|
|
1029
|
+
if (/^#\s*Workspace overview/m.test(value) || /^## Workspace analysis \(from local file inspection\)/m.test(value)) {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
if (this.isGenericV3AgentContent(value))
|
|
1033
|
+
return true;
|
|
1034
|
+
if (/^The V3 agent finished without emitting a dedicated final answer/i.test(value)) {
|
|
1035
|
+
return !/(## Files read|## Directories inspected|### )/i.test(value);
|
|
1036
|
+
}
|
|
1037
|
+
return value.length < 120;
|
|
1038
|
+
}
|
|
1039
|
+
shouldPrintV3FinalContent(text) {
|
|
1040
|
+
if (!text || this.isGenericV3AgentContent(text)) {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
const value = text.trim();
|
|
1044
|
+
if (/^## Workspace analysis \(from local file inspection\)/m.test(value)) {
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
if (/Reconstructed task summary|Workspace analysis \(from local file inspection\)/i.test(value)) {
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
if (!this.v3StreamingStarted) {
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
return !this.hasAlreadyStreamedV3Content(value);
|
|
1054
|
+
}
|
|
1055
|
+
rememberV3ToolEvidence(event, args = {}) {
|
|
1056
|
+
const output = typeof event?.output === 'string' ? event.output.trim() : '';
|
|
1057
|
+
const errorText = typeof event?.error === 'string' ? event.error.trim() : '';
|
|
1058
|
+
const combined = output || errorText;
|
|
1059
|
+
if (!combined) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const target = String(args.path || args.file_path || args.file || args.target || event?.target || '').trim();
|
|
1063
|
+
this.v3LiveToolEvidence.push({
|
|
1064
|
+
name: String(event?.name || event?.tool || 'unknown_tool'),
|
|
1065
|
+
target: target || undefined,
|
|
1066
|
+
arguments: args,
|
|
1067
|
+
output: combined,
|
|
1068
|
+
success: event?.success !== false,
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
buildUserFacingV3RunReport(prompt, workspacePath, options = {}) {
|
|
1072
|
+
const evidenceBody = this.api.formatV3AgentResponse({
|
|
1073
|
+
events: [],
|
|
1074
|
+
liveToolEvidence: this.v3LiveToolEvidence,
|
|
1075
|
+
});
|
|
1076
|
+
const successes = this.v3LiveToolEvidence.filter((entry) => entry.success !== false);
|
|
1077
|
+
const failures = this.v3LiveToolEvidence.filter((entry) => entry.success === false);
|
|
1078
|
+
if (!evidenceBody && successes.length === 0 && failures.length === 0) {
|
|
1079
|
+
return [
|
|
1080
|
+
'# Agent run summary',
|
|
1081
|
+
'',
|
|
1082
|
+
`**Workspace:** ${workspacePath}`,
|
|
1083
|
+
`**Your request:** ${prompt.trim()}`,
|
|
1084
|
+
'',
|
|
1085
|
+
'The agent finished without successfully reading local files, so no grounded overview could be built.',
|
|
1086
|
+
options.serverNote ? `\n**Server note:** ${options.serverNote}` : '',
|
|
1087
|
+
'',
|
|
1088
|
+
'Try `/continue` with: "Read Vigthoria-dominion/package.json, game.js, src/Game.js, and src/factions/, then write a full overview."',
|
|
1089
|
+
].filter(Boolean).join('\n');
|
|
1090
|
+
}
|
|
1091
|
+
const lines = [
|
|
1092
|
+
'# Workspace overview',
|
|
1093
|
+
'',
|
|
1094
|
+
`**Workspace:** ${workspacePath}`,
|
|
1095
|
+
`**Your request:** ${prompt.trim()}`,
|
|
1096
|
+
options.partial
|
|
1097
|
+
? '**Status:** Partial — the agent used its iteration budget before writing a final narrative. The report below is rebuilt from files it actually read on your machine.'
|
|
1098
|
+
: '**Status:** Complete — summary rebuilt from local file inspection.',
|
|
1099
|
+
'',
|
|
1100
|
+
evidenceBody || '_No readable file excerpts were captured._',
|
|
1101
|
+
];
|
|
1102
|
+
if (failures.length > 0) {
|
|
1103
|
+
const uniqueFails = new Map();
|
|
1104
|
+
for (const entry of failures) {
|
|
1105
|
+
const failPath = entry.target || entry.arguments?.path || entry.name;
|
|
1106
|
+
if (!uniqueFails.has(failPath)) {
|
|
1107
|
+
uniqueFails.set(failPath, entry.output.split('\n').find(Boolean)?.slice(0, 140) || entry.output.slice(0, 140));
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (uniqueFails.size > 0 && !/## Paths not found/i.test(evidenceBody || '')) {
|
|
1111
|
+
lines.push('', '## Paths not found (exploration misses)', ...[...uniqueFails.entries()].slice(0, 12).map(([failPath, detail]) => `- \`${failPath}\`${detail ? ` — ${detail}` : ''}`), '', '_These are usually incorrect guesses (for example `Vigthoria-dominion/entities` instead of `Vigthoria-dominion/src/entities`). They do not mean your project is broken._');
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
lines.push('', '---', 'Use `/continue` if you want a longer narrative summary or a deep dive into a specific subfolder.');
|
|
1115
|
+
return lines.join('\n');
|
|
1116
|
+
}
|
|
1117
|
+
printV3UserReport(report) {
|
|
1118
|
+
const value = String(report || '').trim();
|
|
1119
|
+
if (!value || this.isGenericV3AgentContent(value)) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
console.log();
|
|
1123
|
+
console.log(value);
|
|
1124
|
+
console.log();
|
|
1125
|
+
}
|
|
575
1126
|
updateV3AgentSpinner(spinner, event) {
|
|
576
1127
|
if (this.isRawV3StreamPayload(event)) {
|
|
577
1128
|
this.consumeV3StreamPayload(spinner, event).catch((error) => {
|
|
@@ -589,7 +1140,7 @@ export class ChatCommand {
|
|
|
589
1140
|
if (event.type === 'tool_call') {
|
|
590
1141
|
this.v3ToolCallCount += 1;
|
|
591
1142
|
const toolDesc = this.describeV3AgentTool(event.tool || event.name || event.tool_name);
|
|
592
|
-
const toolTarget = event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '';
|
|
1143
|
+
const toolTarget = event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || event.arguments?.query || '';
|
|
593
1144
|
const sanitizedTarget = this.sanitizeServerPath(String(toolTarget));
|
|
594
1145
|
const shortTarget = sanitizedTarget ? ` → ${sanitizedTarget.replace(/\\/g, '/').split('/').slice(-2).join('/')}` : '';
|
|
595
1146
|
const stepLabel = chalk.cyan(` [${this.v3IterationCount}/${this.v3ToolCallCount}]`) + ` ${toolDesc}${shortTarget}`;
|
|
@@ -599,6 +1150,21 @@ export class ChatCommand {
|
|
|
599
1150
|
// Show extra detail for key tools
|
|
600
1151
|
const args = event.arguments || {};
|
|
601
1152
|
const toolName = event.name || event.tool || '';
|
|
1153
|
+
this.v3PendingToolCalls.push({
|
|
1154
|
+
name: String(toolName || 'unknown_tool'),
|
|
1155
|
+
args: args && typeof args === 'object' ? args : {},
|
|
1156
|
+
});
|
|
1157
|
+
if (toolName === 'search_files') {
|
|
1158
|
+
const pattern = String(args.pattern || args.query || '').trim();
|
|
1159
|
+
const searchPath = String(args.path || '.').trim();
|
|
1160
|
+
process.stderr.write(chalk.gray(` search: ${pattern || '(empty)'} in ${this.sanitizeServerPath(searchPath)}\n`));
|
|
1161
|
+
}
|
|
1162
|
+
else if (toolName === 'list_directory') {
|
|
1163
|
+
process.stderr.write(chalk.gray(` list: ${this.sanitizeServerPath(String(args.path || '.'))}${args.recursive ? ' (recursive)' : ''}\n`));
|
|
1164
|
+
}
|
|
1165
|
+
else if (toolName === 'read_file') {
|
|
1166
|
+
process.stderr.write(chalk.gray(` read: ${this.sanitizeServerPath(String(args.path || args.file_path || ''))}\n`));
|
|
1167
|
+
}
|
|
602
1168
|
if ((toolName === 'write_file' || toolName === 'edit_file') && typeof args.content === 'string') {
|
|
603
1169
|
const len = args.content.length;
|
|
604
1170
|
process.stderr.write(chalk.gray(` ${len > 1000 ? Math.round(len / 1024) + ' KB' : len + ' bytes'} content\n`));
|
|
@@ -621,25 +1187,51 @@ export class ChatCommand {
|
|
|
621
1187
|
// Show output for failures, or brief summary for successes — sanitize server paths
|
|
622
1188
|
const rawOutput = typeof event.output === 'string' ? event.output.trim() : '';
|
|
623
1189
|
const output = this.sanitizeServerPath(rawOutput);
|
|
1190
|
+
const pendingIndex = this.v3PendingToolCalls.findIndex((call) => call.name === toolName);
|
|
1191
|
+
const pendingCall = pendingIndex >= 0
|
|
1192
|
+
? this.v3PendingToolCalls.splice(pendingIndex, 1)[0]
|
|
1193
|
+
: this.v3PendingToolCalls.shift();
|
|
1194
|
+
this.rememberV3ToolEvidence(event, pendingCall?.args || {});
|
|
624
1195
|
if (!success && output) {
|
|
625
1196
|
const sanitizedError = this.sanitizeServerPath(typeof event.error === 'string' ? event.error : output);
|
|
626
1197
|
const lines = sanitizedError.split('\n').slice(0, 4);
|
|
627
1198
|
process.stderr.write(chalk.red(` ${lines.join('\n ')}\n`));
|
|
628
1199
|
}
|
|
629
1200
|
else if (success && output && output.length > 0) {
|
|
630
|
-
const
|
|
1201
|
+
const outputLines = output.split('\n').map((line) => line.trimEnd()).filter(Boolean);
|
|
1202
|
+
const firstMeaningfulLine = outputLines.find((line) => !/^## Untrusted workspace data boundary/i.test(line)) || outputLines[0] || '';
|
|
1203
|
+
const brief = firstMeaningfulLine.slice(0, 160);
|
|
631
1204
|
process.stderr.write(chalk.gray(` ${brief}${output.length > 120 ? '…' : ''}\n`));
|
|
632
1205
|
}
|
|
633
1206
|
spinner.start();
|
|
634
1207
|
spinner.text = 'Next step...';
|
|
635
1208
|
return;
|
|
636
1209
|
}
|
|
1210
|
+
if (event.type === 'v3_status') {
|
|
1211
|
+
spinner.text = this.sanitizeServerPath(String(event.content || 'Routing to V3 Agent...'));
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
if (event.type === 'queued') {
|
|
1215
|
+
if (spinner.isSpinning)
|
|
1216
|
+
spinner.stop();
|
|
1217
|
+
process.stderr.write(chalk.yellow(' [Queue] ') + this.sanitizeServerPath(String(event.message || 'Waiting for V3 capacity...')) + '\n');
|
|
1218
|
+
spinner.start();
|
|
1219
|
+
spinner.text = 'Waiting for V3...';
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (event.type === 'heartbeat') {
|
|
1223
|
+
spinner.text = 'V3 Agent is still thinking...';
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
637
1226
|
if (event.type === 'thinking') {
|
|
638
1227
|
this.v3IterationCount += 1;
|
|
639
1228
|
const iterText = this.sanitizeServerPath(event.content || '');
|
|
640
1229
|
if (spinner.isSpinning)
|
|
641
1230
|
spinner.stop();
|
|
642
|
-
process.stderr.write(chalk.cyan(`\n──
|
|
1231
|
+
process.stderr.write(chalk.cyan(`\n── Iteration ${this.v3IterationCount}${iterText ? '' : '...'} ──\n`));
|
|
1232
|
+
if (iterText) {
|
|
1233
|
+
process.stderr.write(chalk.gray(`${iterText}\n`));
|
|
1234
|
+
}
|
|
643
1235
|
spinner.start();
|
|
644
1236
|
spinner.text = 'Analyzing...';
|
|
645
1237
|
return;
|
|
@@ -813,6 +1405,22 @@ export class ChatCommand {
|
|
|
813
1405
|
process.stderr.write(chalk.cyan(' [Start] ') + 'Agent initialized\n');
|
|
814
1406
|
spinner.start();
|
|
815
1407
|
spinner.text = 'Working...';
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
const fallbackStatus = typeof event.status === 'string' ? this.sanitizeServerPath(event.status) : '';
|
|
1411
|
+
const fallbackStage = typeof event.stage === 'string' ? this.sanitizeServerPath(event.stage) : '';
|
|
1412
|
+
const fallbackMessage = typeof event.message === 'string'
|
|
1413
|
+
? this.sanitizeServerPath(event.message)
|
|
1414
|
+
: (typeof event.content === 'string' ? this.sanitizeServerPath(event.content) : '');
|
|
1415
|
+
if (event.type === 'status' || event.type === 'progress' || fallbackStatus || fallbackStage) {
|
|
1416
|
+
if (spinner.isSpinning)
|
|
1417
|
+
spinner.stop();
|
|
1418
|
+
if (fallbackMessage) {
|
|
1419
|
+
process.stderr.write(chalk.cyan(' [V3] ') + `${fallbackMessage}\n`);
|
|
1420
|
+
}
|
|
1421
|
+
spinner.start();
|
|
1422
|
+
spinner.text = fallbackStatus || fallbackStage || 'Working...';
|
|
1423
|
+
return;
|
|
816
1424
|
}
|
|
817
1425
|
}
|
|
818
1426
|
updateOperatorSpinner(spinner, event) {
|
|
@@ -878,6 +1486,7 @@ export class ChatCommand {
|
|
|
878
1486
|
}
|
|
879
1487
|
return;
|
|
880
1488
|
}
|
|
1489
|
+
await this.config.refreshHubModelPreferences().catch(() => null);
|
|
881
1490
|
this.agentMode = options.agent === true;
|
|
882
1491
|
this.operatorMode = options.operator === true;
|
|
883
1492
|
this.workflowTarget = typeof options.workflow === 'string' && options.workflow.trim()
|
|
@@ -886,6 +1495,7 @@ export class ChatCommand {
|
|
|
886
1495
|
this.savePlanToVigFlow = options.savePlan === true;
|
|
887
1496
|
this.jsonOutput = options.json === true;
|
|
888
1497
|
this.autoApprove = options.autoApprove === true || this.jsonOutput;
|
|
1498
|
+
this.personaOverride = options.grant === true ? 'wiener_grant' : null;
|
|
889
1499
|
this.modelExplicitlySelected = Boolean(String(options.model || '').trim());
|
|
890
1500
|
this.currentModel = this.resolveInitialModel(options);
|
|
891
1501
|
this.applyNoAgentGovernance(String(options.model || this.currentModel || ''));
|
|
@@ -956,7 +1566,10 @@ export class ChatCommand {
|
|
|
956
1566
|
bridge.destroy();
|
|
957
1567
|
}
|
|
958
1568
|
}
|
|
959
|
-
|
|
1569
|
+
// Force-exit: undici + chokidar + HTTPS pool keep the Node.js event
|
|
1570
|
+
// loop alive indefinitely in direct prompt mode; a clean exit is safe here.
|
|
1571
|
+
this.api.destroy();
|
|
1572
|
+
process.exit(process.exitCode ?? 0);
|
|
960
1573
|
}
|
|
961
1574
|
await this.startInteractiveChat();
|
|
962
1575
|
const bridge = getBridgeClient();
|
|
@@ -998,11 +1611,57 @@ export class ChatCommand {
|
|
|
998
1611
|
}
|
|
999
1612
|
fs.mkdirSync(this.currentProjectPath, { recursive: true });
|
|
1000
1613
|
}
|
|
1614
|
+
shouldStartWorkspaceWatcher(workspaceRoot) {
|
|
1615
|
+
if (!workspaceRoot) {
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
const resolved = path.resolve(workspaceRoot);
|
|
1619
|
+
if (!fs.existsSync(resolved)) {
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
let stat;
|
|
1623
|
+
try {
|
|
1624
|
+
stat = fs.statSync(resolved);
|
|
1625
|
+
}
|
|
1626
|
+
catch {
|
|
1627
|
+
return false;
|
|
1628
|
+
}
|
|
1629
|
+
if (!stat.isDirectory()) {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
const homeDir = path.resolve(os.homedir());
|
|
1633
|
+
const normalized = resolved.replace(/\\/g, '/');
|
|
1634
|
+
const normalizedHome = homeDir.replace(/\\/g, '/');
|
|
1635
|
+
// Guardrail: do not watch broad home/root scopes on interactive shells.
|
|
1636
|
+
if (normalized.toLowerCase() === normalizedHome.toLowerCase()) {
|
|
1637
|
+
if (!this.jsonOutput) {
|
|
1638
|
+
console.log(chalk.gray('Info: workspace watcher disabled for home directory scope.'));
|
|
1639
|
+
}
|
|
1640
|
+
return false;
|
|
1641
|
+
}
|
|
1642
|
+
if (/^[a-zA-Z]:\/$/.test(normalized) || normalized === '/') {
|
|
1643
|
+
if (!this.jsonOutput) {
|
|
1644
|
+
console.log(chalk.gray('Info: workspace watcher disabled for filesystem root scope.'));
|
|
1645
|
+
}
|
|
1646
|
+
return false;
|
|
1647
|
+
}
|
|
1648
|
+
return true;
|
|
1649
|
+
}
|
|
1001
1650
|
resolveProjectPath(options) {
|
|
1002
1651
|
const requestedProject = String(options.project || '').trim();
|
|
1003
1652
|
if (requestedProject) {
|
|
1004
1653
|
return path.resolve(requestedProject);
|
|
1005
1654
|
}
|
|
1655
|
+
// Check if prompt contains an explicit local path provided by the user
|
|
1656
|
+
if (options.prompt) {
|
|
1657
|
+
const explicitPath = this.resolvePromptWorkspacePath(options.prompt, process.cwd());
|
|
1658
|
+
if (explicitPath) {
|
|
1659
|
+
if (!this.jsonOutput) {
|
|
1660
|
+
console.log(chalk.gray(`📁 Using project path from prompt: ${explicitPath}`));
|
|
1661
|
+
}
|
|
1662
|
+
return explicitPath;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1006
1665
|
if (this.shouldUseManagedWorkspace(options)) {
|
|
1007
1666
|
const rootPath = this.getManagedWorkspaceRoot();
|
|
1008
1667
|
fs.mkdirSync(rootPath, { recursive: true });
|
|
@@ -1465,6 +2124,26 @@ export class ChatCommand {
|
|
|
1465
2124
|
}
|
|
1466
2125
|
}
|
|
1467
2126
|
async runSimplePrompt(prompt) {
|
|
2127
|
+
if (!this.directPromptMode && !this.operatorMode) {
|
|
2128
|
+
const promptToRun = this.isConfirmationFollowUp(prompt) && this.lastActionableUserInput && this.isRepoGroundedPrompt(this.lastActionableUserInput)
|
|
2129
|
+
? this.lastActionableUserInput
|
|
2130
|
+
: prompt;
|
|
2131
|
+
if (this.isRepoGroundedPrompt(promptToRun) || /\b(build|implement|complete|fix|create|make|edit|write|change|finish)\b/i.test(promptToRun)) {
|
|
2132
|
+
if (!this.tools) {
|
|
2133
|
+
throw new Error('Agent tools are not initialized.');
|
|
2134
|
+
}
|
|
2135
|
+
this.agentMode = true;
|
|
2136
|
+
this.syncInteractiveModeModel('agent');
|
|
2137
|
+
if (this.currentSession) {
|
|
2138
|
+
this.currentSession.agentMode = true;
|
|
2139
|
+
this.currentSession.model = this.currentModel;
|
|
2140
|
+
}
|
|
2141
|
+
console.log(chalk.yellow('This request needs file access, so I am switching to Agent mode and working in this workspace now.'));
|
|
2142
|
+
console.log(chalk.gray('You do not need to confirm with "yes"; I will continue until the agent run finishes or reports a blocker.'));
|
|
2143
|
+
await this.runAgentTurn(promptToRun);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
1468
2147
|
this.lastActionableUserInput = prompt;
|
|
1469
2148
|
// For direct --prompt mode with simple prompts, use a minimal system
|
|
1470
2149
|
// message to avoid polluting the response with tool/platform context.
|
|
@@ -1525,7 +2204,7 @@ export class ChatCommand {
|
|
|
1525
2204
|
const response = await this.callApi('Send chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel));
|
|
1526
2205
|
if (spinner)
|
|
1527
2206
|
spinner.stop();
|
|
1528
|
-
const finalText = (response.message || '').trim();
|
|
2207
|
+
const finalText = this.sanitizeDirectModeOutput(this.stripHiddenThoughtBlocks(response.message || '')).trim();
|
|
1529
2208
|
const effectiveModel = String(response.model || this.currentModel);
|
|
1530
2209
|
const metadata = this.modelGovernanceFallback
|
|
1531
2210
|
? { modelFallback: this.modelGovernanceFallback }
|
|
@@ -1570,21 +2249,32 @@ export class ChatCommand {
|
|
|
1570
2249
|
}
|
|
1571
2250
|
}
|
|
1572
2251
|
}
|
|
1573
|
-
async runAgentTurn(prompt) {
|
|
2252
|
+
async runAgentTurn(prompt, options = {}) {
|
|
1574
2253
|
if (!this.tools) {
|
|
1575
2254
|
throw new Error('Agent tools are not initialized.');
|
|
1576
2255
|
}
|
|
2256
|
+
this.bindPromptWorkspace(prompt);
|
|
1577
2257
|
const requiresV3Workflow = this.shouldRequireV3AgentWorkflow(prompt);
|
|
1578
2258
|
const handledByDirectFileFlow = await this.tryDirectSingleFileFlow(prompt);
|
|
1579
2259
|
if (handledByDirectFileFlow) {
|
|
1580
2260
|
this.saveSession();
|
|
1581
2261
|
return;
|
|
1582
2262
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
2263
|
+
// Prime the message context with the target file when the direct-file flow was
|
|
2264
|
+
// bypassed (e.g. HTML files routed to V3) so the local agent loop has file
|
|
2265
|
+
// awareness and the ⚙ Executing: read_file banner is always emitted.
|
|
2266
|
+
await this.primeBypassedTargetFileContext(prompt);
|
|
2267
|
+
if (!options.skipLocalLoop && !options.forceV3 && this.shouldPreferLocalAgentLoop(prompt)) {
|
|
2268
|
+
const completed = await this.runLocalAgentLoop(prompt);
|
|
2269
|
+
if (completed) {
|
|
2270
|
+
this.saveSession();
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
if (!this.jsonOutput) {
|
|
2274
|
+
console.log(chalk.yellow('Local chat backend unavailable (self-hosted inference may be offline). Trying V3 agent workflow...'));
|
|
2275
|
+
}
|
|
1586
2276
|
}
|
|
1587
|
-
const handledByV3Workflow = await this.tryV3AgentWorkflow(prompt);
|
|
2277
|
+
const handledByV3Workflow = await this.tryV3AgentWorkflow(prompt, options);
|
|
1588
2278
|
if (handledByV3Workflow) {
|
|
1589
2279
|
this.saveSession();
|
|
1590
2280
|
return;
|
|
@@ -1598,10 +2288,56 @@ export class ChatCommand {
|
|
|
1598
2288
|
}
|
|
1599
2289
|
await this.runLocalAgentLoop(prompt);
|
|
1600
2290
|
}
|
|
2291
|
+
localAgentIterationCount = 0;
|
|
2292
|
+
buildLocalAgentChatOptions(preflight, onRouteAttempt) {
|
|
2293
|
+
const preferredRoute = preflight?.endpoint
|
|
2294
|
+
? this.api.mapPreflightEndpointToRoute(preflight.endpoint)
|
|
2295
|
+
: undefined;
|
|
2296
|
+
return {
|
|
2297
|
+
fastFail: true,
|
|
2298
|
+
singleRoute: true,
|
|
2299
|
+
stream: true,
|
|
2300
|
+
connectTimeoutMs: Number.parseInt(process.env.VIGTHORIA_CHAT_CONNECT_TIMEOUT_MS || '20000', 10),
|
|
2301
|
+
idleTimeoutMs: Number.parseInt(process.env.VIGTHORIA_CHAT_IDLE_TIMEOUT_MS || '120000', 10),
|
|
2302
|
+
preferredRoute: preferredRoute || 'coder',
|
|
2303
|
+
onRouteAttempt,
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
printChatModelPreflight(preflight) {
|
|
2307
|
+
if (this.jsonOutput) {
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
console.log(chalk.gray('Model backend preflight:'));
|
|
2311
|
+
for (const route of preflight.routes) {
|
|
2312
|
+
const marker = route.ok ? chalk.green(' ✓') : chalk.red(' ✗');
|
|
2313
|
+
const detail = route.error ? chalk.gray(` — ${route.error}`) : '';
|
|
2314
|
+
console.log(`${marker} ${route.name}${detail}`);
|
|
2315
|
+
}
|
|
2316
|
+
if (preflight.healthy) {
|
|
2317
|
+
console.log(chalk.gray(`Using: ${preflight.endpoint}`));
|
|
2318
|
+
}
|
|
2319
|
+
else if (preflight.error) {
|
|
2320
|
+
console.log(chalk.yellow(preflight.error));
|
|
2321
|
+
}
|
|
2322
|
+
console.log();
|
|
2323
|
+
}
|
|
1601
2324
|
async runLocalAgentLoop(prompt) {
|
|
1602
2325
|
if (!this.tools) {
|
|
1603
2326
|
throw new Error('Agent tools are not initialized.');
|
|
1604
2327
|
}
|
|
2328
|
+
this.localAgentIterationCount = 0;
|
|
2329
|
+
const runtime = this.getRuntimeEnvironmentContext();
|
|
2330
|
+
if (!this.jsonOutput) {
|
|
2331
|
+
console.log();
|
|
2332
|
+
console.log(chalk.gray('━━━ ROUTING DECISION ━━━'));
|
|
2333
|
+
console.log(chalk.gray('Reason: local-machine-agent-loop'));
|
|
2334
|
+
console.log(chalk.gray(`Platform: ${runtime.platform}`));
|
|
2335
|
+
console.log(chalk.gray(`Machine scope: ${runtime.machineScope}`));
|
|
2336
|
+
console.log(chalk.gray(`Workspace: ${runtime.workspacePath}`));
|
|
2337
|
+
console.log(chalk.gray('Tools execute on your local filesystem.'));
|
|
2338
|
+
console.log(chalk.gray('━'.repeat(30)));
|
|
2339
|
+
console.log();
|
|
2340
|
+
}
|
|
1605
2341
|
this.lastActionableUserInput = prompt;
|
|
1606
2342
|
this.directToolContinuationCount = 0;
|
|
1607
2343
|
this.agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
|
|
@@ -1610,12 +2346,50 @@ export class ChatCommand {
|
|
|
1610
2346
|
this.ensureAgentSystemPrompt();
|
|
1611
2347
|
this.messages.push({ role: 'user', content: this.buildScopedUserPrompt(prompt) });
|
|
1612
2348
|
this.saveSession();
|
|
2349
|
+
const preflightSpinner = this.jsonOutput
|
|
2350
|
+
? null
|
|
2351
|
+
: createSpinner({ text: 'Checking model connection (preflight)...', spinner: 'clock' }).start();
|
|
2352
|
+
let preflight;
|
|
2353
|
+
try {
|
|
2354
|
+
preflight = await this.api.runChatModelPreflight(this.currentModel);
|
|
2355
|
+
}
|
|
2356
|
+
finally {
|
|
2357
|
+
if (preflightSpinner) {
|
|
2358
|
+
preflightSpinner.stop();
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
this.printChatModelPreflight(preflight);
|
|
2362
|
+
if (!preflight.healthy) {
|
|
2363
|
+
return false;
|
|
2364
|
+
}
|
|
1613
2365
|
const maxTurns = 10;
|
|
2366
|
+
const preferredRoute = preflight?.endpoint
|
|
2367
|
+
? (this.api.mapPreflightEndpointToRoute(preflight.endpoint) || 'coder')
|
|
2368
|
+
: 'coder';
|
|
1614
2369
|
for (let turn = 0; turn < maxTurns; turn += 1) {
|
|
1615
|
-
const spinner = this.jsonOutput ? null : createSpinner({ text: turn === 0 ? 'Planning...' : 'Continuing...', spinner: 'clock' }).start();
|
|
2370
|
+
const spinner = this.jsonOutput ? null : createSpinner({ text: turn === 0 ? 'Planning first tool step...' : 'Continuing...', spinner: 'clock' }).start();
|
|
2371
|
+
let streamedVisible = '';
|
|
1616
2372
|
let response;
|
|
1617
2373
|
try {
|
|
1618
|
-
response = await this.callApi('Send agent chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel
|
|
2374
|
+
response = await this.callApi('Send agent chat message', () => this.api.chat(this.getMessagesForModel({ compact: turn === 0 }), this.currentModel, false, {
|
|
2375
|
+
fastFail: true,
|
|
2376
|
+
singleRoute: true,
|
|
2377
|
+
stream: true,
|
|
2378
|
+
connectTimeoutMs: Number.parseInt(process.env.VIGTHORIA_CHAT_CONNECT_TIMEOUT_MS || '20000', 10),
|
|
2379
|
+
idleTimeoutMs: Number.parseInt(process.env.VIGTHORIA_CHAT_IDLE_TIMEOUT_MS || '120000', 10),
|
|
2380
|
+
preferredRoute,
|
|
2381
|
+
onRouteAttempt: (routeLabel) => {
|
|
2382
|
+
if (spinner) {
|
|
2383
|
+
spinner.text = routeLabel;
|
|
2384
|
+
}
|
|
2385
|
+
},
|
|
2386
|
+
onStreamDelta: (chunk) => {
|
|
2387
|
+
streamedVisible += chunk;
|
|
2388
|
+
if (spinner) {
|
|
2389
|
+
spinner.text = streamedVisible.trim().slice(-72) || 'Streaming model output...';
|
|
2390
|
+
}
|
|
2391
|
+
},
|
|
2392
|
+
}), 0);
|
|
1619
2393
|
}
|
|
1620
2394
|
catch (firstErr) {
|
|
1621
2395
|
// If we already gathered evidence and the model API fails on a
|
|
@@ -1624,7 +2398,14 @@ export class ChatCommand {
|
|
|
1624
2398
|
this.logger.debug('Agent continuation API call failed, retrying once...');
|
|
1625
2399
|
try {
|
|
1626
2400
|
await new Promise(r => setTimeout(r, 2000));
|
|
1627
|
-
response = await this.callApi('Retry agent chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel
|
|
2401
|
+
response = await this.callApi('Retry agent chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel, false, {
|
|
2402
|
+
fastFail: true,
|
|
2403
|
+
singleRoute: true,
|
|
2404
|
+
stream: true,
|
|
2405
|
+
connectTimeoutMs: Number.parseInt(process.env.VIGTHORIA_CHAT_CONNECT_TIMEOUT_MS || '20000', 10),
|
|
2406
|
+
idleTimeoutMs: Number.parseInt(process.env.VIGTHORIA_CHAT_IDLE_TIMEOUT_MS || '120000', 10),
|
|
2407
|
+
preferredRoute,
|
|
2408
|
+
}), 0);
|
|
1628
2409
|
}
|
|
1629
2410
|
catch (retryErr) {
|
|
1630
2411
|
// Retry also failed — synthesize an answer from evidence
|
|
@@ -1647,13 +2428,57 @@ export class ChatCommand {
|
|
|
1647
2428
|
console.log(fallbackContent);
|
|
1648
2429
|
}
|
|
1649
2430
|
this.saveSession();
|
|
1650
|
-
return;
|
|
2431
|
+
return true;
|
|
1651
2432
|
}
|
|
1652
2433
|
throw retryErr;
|
|
1653
2434
|
}
|
|
1654
2435
|
}
|
|
1655
2436
|
else {
|
|
1656
|
-
|
|
2437
|
+
const cliErr = firstErr instanceof CLIError ? firstErr : classifyError(firstErr);
|
|
2438
|
+
const formatted = formatCLIError(cliErr);
|
|
2439
|
+
if (spinner)
|
|
2440
|
+
spinner.stop();
|
|
2441
|
+
this.rememberBrainEvent('issue', 'Agent model request failed: ' + formatted, 'agent');
|
|
2442
|
+
if (turn === 0 && (cliErr.category === 'model_backend' || cliErr.category === 'network' || cliErr.category === 'timeout')) {
|
|
2443
|
+
if (!this.jsonOutput) {
|
|
2444
|
+
this.logger.error(formatted);
|
|
2445
|
+
const transport = this.api.getLastChatTransportErrors();
|
|
2446
|
+
if (transport.length > 0) {
|
|
2447
|
+
console.log(chalk.gray(`Routes tried: ${transport.slice(0, 3).join(' | ')}`));
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
else {
|
|
2451
|
+
process.exitCode = 1;
|
|
2452
|
+
console.log(JSON.stringify({
|
|
2453
|
+
success: false,
|
|
2454
|
+
mode: 'agent',
|
|
2455
|
+
model: this.currentModel,
|
|
2456
|
+
partial: false,
|
|
2457
|
+
content: '',
|
|
2458
|
+
error: formatted,
|
|
2459
|
+
errorCategory: cliErr.category,
|
|
2460
|
+
metadata: { executionPath: 'local-agent-loop' },
|
|
2461
|
+
}, null, 2));
|
|
2462
|
+
}
|
|
2463
|
+
return false;
|
|
2464
|
+
}
|
|
2465
|
+
if (this.jsonOutput) {
|
|
2466
|
+
process.exitCode = 1;
|
|
2467
|
+
console.log(JSON.stringify({
|
|
2468
|
+
success: false,
|
|
2469
|
+
mode: 'agent',
|
|
2470
|
+
model: this.currentModel,
|
|
2471
|
+
partial: false,
|
|
2472
|
+
content: '',
|
|
2473
|
+
error: formatted,
|
|
2474
|
+
errorCategory: cliErr.category,
|
|
2475
|
+
metadata: { executionPath: 'local-agent-loop' },
|
|
2476
|
+
}, null, 2));
|
|
2477
|
+
}
|
|
2478
|
+
else {
|
|
2479
|
+
this.logger.error(formatted);
|
|
2480
|
+
}
|
|
2481
|
+
return true;
|
|
1657
2482
|
}
|
|
1658
2483
|
}
|
|
1659
2484
|
if (spinner)
|
|
@@ -1663,6 +2488,11 @@ export class ChatCommand {
|
|
|
1663
2488
|
this.messages.push({ role: 'assistant', content: assistantMessage });
|
|
1664
2489
|
const toolCalls = this.extractToolCalls(assistantMessage);
|
|
1665
2490
|
const visibleText = this.stripToolPayloads(assistantMessage).trim();
|
|
2491
|
+
if (visibleText && !this.jsonOutput) {
|
|
2492
|
+
this.localAgentIterationCount += 1;
|
|
2493
|
+
console.log(chalk.cyan(`\n── Iteration ${this.localAgentIterationCount} ──`));
|
|
2494
|
+
console.log(chalk.gray(visibleText));
|
|
2495
|
+
}
|
|
1666
2496
|
getBridgeClient()?.emitModelResponse({
|
|
1667
2497
|
model: this.currentModel,
|
|
1668
2498
|
chars: assistantMessage.length,
|
|
@@ -1731,7 +2561,7 @@ export class ChatCommand {
|
|
|
1731
2561
|
console.log(finalContent);
|
|
1732
2562
|
}
|
|
1733
2563
|
this.saveSession();
|
|
1734
|
-
return;
|
|
2564
|
+
return true;
|
|
1735
2565
|
}
|
|
1736
2566
|
await this.executeToolCalls(toolCalls);
|
|
1737
2567
|
this.directToolContinuationCount += 1;
|
|
@@ -1765,7 +2595,7 @@ export class ChatCommand {
|
|
|
1765
2595
|
else {
|
|
1766
2596
|
this.logger.error(errorMsg);
|
|
1767
2597
|
}
|
|
1768
|
-
return;
|
|
2598
|
+
return true;
|
|
1769
2599
|
}
|
|
1770
2600
|
}
|
|
1771
2601
|
if (this.jsonOutput) {
|
|
@@ -1786,6 +2616,36 @@ export class ChatCommand {
|
|
|
1786
2616
|
console.log('Task complete.');
|
|
1787
2617
|
}
|
|
1788
2618
|
this.saveSession();
|
|
2619
|
+
return true;
|
|
2620
|
+
}
|
|
2621
|
+
async primeBypassedTargetFileContext(prompt) {
|
|
2622
|
+
if (!this.directPromptMode || !this.tools) {
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
const targetFile = this.inferTargetFileFromPrompt(prompt);
|
|
2626
|
+
if (!targetFile) {
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
// Only prime if the direct-file flow was bypassed for this file (e.g. HTML routed to V3).
|
|
2630
|
+
// This ensures the local agent loop always has full file awareness before planning.
|
|
2631
|
+
if (!this.shouldBypassDirectSingleFileFlow(targetFile, prompt)) {
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
const readCall = {
|
|
2635
|
+
tool: 'read_file',
|
|
2636
|
+
args: { path: targetFile },
|
|
2637
|
+
};
|
|
2638
|
+
if (!this.jsonOutput) {
|
|
2639
|
+
console.log(chalk.cyan(`⚙ Executing: ${readCall.tool}`));
|
|
2640
|
+
}
|
|
2641
|
+
const readResult = await this.tools.execute(readCall);
|
|
2642
|
+
const readSummary = this.formatToolResult(readCall, readResult);
|
|
2643
|
+
if (!this.jsonOutput) {
|
|
2644
|
+
console.log(readResult.success ? chalk.gray(readSummary) : chalk.red(readSummary));
|
|
2645
|
+
}
|
|
2646
|
+
if (readResult.success && readResult.output) {
|
|
2647
|
+
this.messages.push({ role: 'system', content: readSummary });
|
|
2648
|
+
}
|
|
1789
2649
|
}
|
|
1790
2650
|
async tryDirectSingleFileFlow(prompt) {
|
|
1791
2651
|
if (!this.directPromptMode || !this.tools) {
|
|
@@ -1942,7 +2802,16 @@ export class ChatCommand {
|
|
|
1942
2802
|
'Do not reinterpret this confirmation as a new website, landing page, template, or index.html task.',
|
|
1943
2803
|
].join('\n');
|
|
1944
2804
|
}
|
|
1945
|
-
async tryV3AgentWorkflow(prompt) {
|
|
2805
|
+
async tryV3AgentWorkflow(prompt, options = {}) {
|
|
2806
|
+
// Extract explicit workspace path from prompt (if provided by user)
|
|
2807
|
+
let promptWorkspacePath = null;
|
|
2808
|
+
try {
|
|
2809
|
+
promptWorkspacePath = this.resolvePromptWorkspacePath(prompt, this.currentProjectPath);
|
|
2810
|
+
}
|
|
2811
|
+
catch {
|
|
2812
|
+
// Path extraction failed, use default workspace
|
|
2813
|
+
}
|
|
2814
|
+
const workspacePath = promptWorkspacePath || this.currentProjectPath;
|
|
1946
2815
|
const contextualPrompt = this.buildContextualAgentPrompt(prompt);
|
|
1947
2816
|
if (contextualPrompt === prompt && !this.isConfirmationFollowUp(prompt)) {
|
|
1948
2817
|
this.lastActionableUserInput = prompt;
|
|
@@ -1950,11 +2819,28 @@ export class ChatCommand {
|
|
|
1950
2819
|
this.messages.push({ role: 'user', content: contextualPrompt });
|
|
1951
2820
|
const runtimeContext = await this.getPromptRuntimeContext(contextualPrompt);
|
|
1952
2821
|
const routingPolicy = this.resolveAgentExecutionPolicy(contextualPrompt);
|
|
2822
|
+
// STREAMING: Log routing decision transparently to user
|
|
2823
|
+
if (!this.jsonOutput) {
|
|
2824
|
+
console.log();
|
|
2825
|
+
console.log(chalk.gray('━━━ ROUTING DECISION ━━━'));
|
|
2826
|
+
console.log(chalk.gray(`Reason: ${routingPolicy.routeReason}`));
|
|
2827
|
+
console.log(chalk.gray(`Model: ${routingPolicy.selectedModel}`));
|
|
2828
|
+
console.log(chalk.gray(`Cloud Eligible: ${routingPolicy.cloudEligible}`));
|
|
2829
|
+
console.log(chalk.gray(`Cloud Selected: ${routingPolicy.cloudSelected}`));
|
|
2830
|
+
if (routingPolicy.heavyTask) {
|
|
2831
|
+
console.log(chalk.gray(`Task Complexity: HEAVY`));
|
|
2832
|
+
}
|
|
2833
|
+
console.log(chalk.gray('━'.repeat(30)));
|
|
2834
|
+
console.log();
|
|
2835
|
+
}
|
|
1953
2836
|
// Reset streaming counters for new workflow
|
|
1954
2837
|
this.v3IterationCount = 0;
|
|
1955
2838
|
this.v3ToolCallCount = 0;
|
|
1956
2839
|
this.v3LastActivity = Date.now();
|
|
1957
2840
|
this.v3StreamingStarted = false;
|
|
2841
|
+
this.v3StreamedTextBuffer = '';
|
|
2842
|
+
this.v3LiveToolEvidence = [];
|
|
2843
|
+
this.v3PendingToolCalls = [];
|
|
1958
2844
|
const taskDisplay = new TaskDisplay(['Analyse workspace', 'Execute tasks', 'Validate output', 'Self-heal'], !this.jsonOutput);
|
|
1959
2845
|
taskDisplay.start(0);
|
|
1960
2846
|
const spinner = this.jsonOutput ? null : createSpinner({
|
|
@@ -2018,25 +2904,59 @@ export class ChatCommand {
|
|
|
2018
2904
|
const executionPrompt = this.buildExecutionPrompt(contextualPrompt);
|
|
2019
2905
|
const agentTaskType = this.inferAgentTaskType(contextualPrompt);
|
|
2020
2906
|
const workspaceContext = {
|
|
2021
|
-
workspacePath:
|
|
2022
|
-
projectPath:
|
|
2023
|
-
targetPath:
|
|
2907
|
+
workspacePath: workspacePath,
|
|
2908
|
+
projectPath: workspacePath,
|
|
2909
|
+
targetPath: workspacePath,
|
|
2024
2910
|
...runtimeContext,
|
|
2025
2911
|
};
|
|
2912
|
+
if (!this.jsonOutput && !this.directPromptMode) {
|
|
2913
|
+
try {
|
|
2914
|
+
const snapshot = this.api.getAgentWorkspaceSnapshot(workspacePath);
|
|
2915
|
+
console.log(chalk.gray(`Workspace sync: ${snapshot.fileCount} files indexed from ${workspacePath}`));
|
|
2916
|
+
if (snapshot.paths.length > 0) {
|
|
2917
|
+
console.log(chalk.gray(`Workspace index: ${snapshot.paths.slice(0, 5).map((filePath) => filePath.replace(/\\/g, '/')).join(', ')}${snapshot.paths.length > 5 ? ', ...' : ''}`));
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
catch {
|
|
2921
|
+
console.log(chalk.gray(`Workspace sync: preparing ${workspacePath}`));
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2026
2924
|
// Start workspace watcher for bidirectional real-time sync
|
|
2027
2925
|
let watcher = null;
|
|
2028
|
-
|
|
2926
|
+
let workspaceWs = null;
|
|
2927
|
+
const stopWorkspaceSync = () => {
|
|
2928
|
+
watcher?.stop();
|
|
2929
|
+
if (workspaceWs) {
|
|
2930
|
+
workspaceWs.disconnect();
|
|
2931
|
+
workspaceWs = null;
|
|
2932
|
+
}
|
|
2933
|
+
};
|
|
2934
|
+
if (this.shouldStartWorkspaceWatcher(workspacePath)) {
|
|
2029
2935
|
watcher = new WorkspaceWatcher({
|
|
2030
|
-
workspaceRoot:
|
|
2936
|
+
workspaceRoot: workspacePath,
|
|
2031
2937
|
onFileChange: (relativePath, content, action) => {
|
|
2032
2938
|
this.logger.debug(`Local change detected: ${action} ${relativePath}`);
|
|
2939
|
+
workspaceWs?.syncFile(relativePath, content, action);
|
|
2033
2940
|
},
|
|
2034
2941
|
});
|
|
2035
2942
|
watcher.start();
|
|
2036
2943
|
}
|
|
2037
2944
|
try {
|
|
2945
|
+
// Ensure the V3 service key is available before health check
|
|
2946
|
+
await this.api.ensureV3ServiceKey();
|
|
2947
|
+
if (spinner)
|
|
2948
|
+
this.updateV3AgentSpinner(spinner, { type: 'v3_status', content: 'Checking V3 connection (preflight)...' });
|
|
2949
|
+
const healthCheck = await this.api.runV3HealthCheck({ soft: true });
|
|
2950
|
+
if (!healthCheck.healthy) {
|
|
2951
|
+
this.logger.warn('V3 health probe did not confirm readiness; starting SSE agent stream anyway.');
|
|
2952
|
+
if (spinner)
|
|
2953
|
+
this.updateV3AgentSpinner(spinner, { type: 'v3_status', content: 'Starting V3 agent stream...' });
|
|
2954
|
+
}
|
|
2955
|
+
else if (spinner) {
|
|
2956
|
+
this.updateV3AgentSpinner(spinner, { type: 'v3_status', content: 'Connected to V3. Starting agent execution...' });
|
|
2957
|
+
}
|
|
2038
2958
|
const workflowPromise = this.api.runV3AgentWorkflow(executionPrompt, {
|
|
2039
|
-
workspace: { path:
|
|
2959
|
+
workspace: { path: workspacePath },
|
|
2040
2960
|
...workspaceContext,
|
|
2041
2961
|
agentTaskType,
|
|
2042
2962
|
executionSurface: 'cli',
|
|
@@ -2052,6 +2972,32 @@ export class ChatCommand {
|
|
|
2052
2972
|
rawPrompt: prompt,
|
|
2053
2973
|
contextualPrompt,
|
|
2054
2974
|
history: this.getMessagesForModel(),
|
|
2975
|
+
clientToolExecution: true,
|
|
2976
|
+
liveToolEvidence: this.v3LiveToolEvidence,
|
|
2977
|
+
onClientToolExecute: (event) => this.executeClientV3Tool(event),
|
|
2978
|
+
onWorkspaceContext: async ({ contextId, serverWorkspaceRoot }) => {
|
|
2979
|
+
if (workspaceWs || !contextId || !serverWorkspaceRoot || !this.shouldStartWorkspaceWatcher(workspacePath)) {
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
const headers = await this.api.getV3AgentHeaders();
|
|
2983
|
+
const token = String(headers.Authorization?.replace(/^Bearer\s+/i, '')
|
|
2984
|
+
|| headers['X-V3-Service-Key']
|
|
2985
|
+
|| headers['x-v3-service-key']
|
|
2986
|
+
|| '').trim();
|
|
2987
|
+
if (!token) {
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
const baseUrl = this.api.getV3AgentBaseUrls(false)[0] || 'https://coder.vigthoria.io';
|
|
2991
|
+
const serverUrl = baseUrl.replace(/^http/i, 'ws');
|
|
2992
|
+
workspaceWs = new WorkspaceWSClient({
|
|
2993
|
+
serverUrl,
|
|
2994
|
+
token,
|
|
2995
|
+
contextId,
|
|
2996
|
+
workspaceRoot: serverWorkspaceRoot,
|
|
2997
|
+
});
|
|
2998
|
+
workspaceWs.connect();
|
|
2999
|
+
this.logger.debug(`Workspace WS sync bound to context ${contextId}`);
|
|
3000
|
+
},
|
|
2055
3001
|
...runtimeContext,
|
|
2056
3002
|
onStreamEvent: (event) => {
|
|
2057
3003
|
if (event.type === 'plan') {
|
|
@@ -2111,6 +3057,34 @@ export class ChatCommand {
|
|
|
2111
3057
|
if (this.v3StreamingStarted) {
|
|
2112
3058
|
process.stdout.write('\n');
|
|
2113
3059
|
}
|
|
3060
|
+
let finalContent = String(response.content || '').trim();
|
|
3061
|
+
const needsUserReport = this.inferAgentTaskType(prompt) === 'analysis'
|
|
3062
|
+
|| this.isThinV3Summary(finalContent)
|
|
3063
|
+
|| this.v3LiveToolEvidence.length > 0;
|
|
3064
|
+
if (needsUserReport) {
|
|
3065
|
+
const userReport = this.buildUserFacingV3RunReport(prompt, workspacePath, {
|
|
3066
|
+
partial: response.partial === true || this.isThinV3Summary(finalContent),
|
|
3067
|
+
serverNote: finalContent && !this.isThinV3Summary(finalContent) ? finalContent : undefined,
|
|
3068
|
+
});
|
|
3069
|
+
if (userReport && !this.isThinV3Summary(userReport)) {
|
|
3070
|
+
finalContent = userReport;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
else if (this.isThinV3Summary(finalContent) && this.v3LiveToolEvidence.length > 0) {
|
|
3074
|
+
const evidenceSummary = this.api.formatV3AgentResponse({
|
|
3075
|
+
events: [],
|
|
3076
|
+
liveToolEvidence: this.v3LiveToolEvidence,
|
|
3077
|
+
});
|
|
3078
|
+
if (evidenceSummary && !this.isThinV3Summary(evidenceSummary)) {
|
|
3079
|
+
finalContent = evidenceSummary;
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
response.content = finalContent || response.content;
|
|
3083
|
+
let v3UserReportPrinted = false;
|
|
3084
|
+
if (!this.jsonOutput && finalContent && needsUserReport) {
|
|
3085
|
+
this.printV3UserReport(finalContent);
|
|
3086
|
+
v3UserReportPrinted = true;
|
|
3087
|
+
}
|
|
2114
3088
|
const previewGate = (response.metadata?.previewGate || null);
|
|
2115
3089
|
const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
|
|
2116
3090
|
const success = previewGate?.required === true
|
|
@@ -2123,7 +3097,7 @@ export class ChatCommand {
|
|
|
2123
3097
|
}
|
|
2124
3098
|
this.logger.warn('Falling back to legacy CLI agent loop');
|
|
2125
3099
|
this.logger.debug(`V3 agent workflow returned an incomplete result: ${previewGate?.error || 'workspace changes were not fully validated'}`);
|
|
2126
|
-
|
|
3100
|
+
stopWorkspaceSync();
|
|
2127
3101
|
return false;
|
|
2128
3102
|
}
|
|
2129
3103
|
const errorMessage = `V3 agent workflow returned an incomplete result and legacy fallback is disabled. ${previewGate?.error || 'Workspace changes were not fully validated.'}`;
|
|
@@ -2144,7 +3118,7 @@ export class ChatCommand {
|
|
|
2144
3118
|
metadata: { executionPath: 'v3-agent', previewGate },
|
|
2145
3119
|
}, null, 2));
|
|
2146
3120
|
}
|
|
2147
|
-
|
|
3121
|
+
stopWorkspaceSync();
|
|
2148
3122
|
return true;
|
|
2149
3123
|
}
|
|
2150
3124
|
if (!this.jsonOutput && previewGate?.required && previewGate?.passed !== true && workspaceHasOutput) {
|
|
@@ -2164,16 +3138,32 @@ export class ChatCommand {
|
|
|
2164
3138
|
}, null, 2));
|
|
2165
3139
|
}
|
|
2166
3140
|
else if (this.v3StreamingStarted) {
|
|
2167
|
-
// Content
|
|
2168
|
-
|
|
3141
|
+
// Content may have been streamed already, but some V3 runs only stream
|
|
3142
|
+
// status/tool messages and keep the useful final report in response.content.
|
|
3143
|
+
if (!v3UserReportPrinted && !this.jsonOutput) {
|
|
2169
3144
|
console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
|
|
2170
3145
|
}
|
|
3146
|
+
if (!v3UserReportPrinted && response.content && this.shouldPrintV3FinalContent(response.content)) {
|
|
3147
|
+
console.log(response.content);
|
|
3148
|
+
}
|
|
3149
|
+
else if (!v3UserReportPrinted && response.content && this.isThinV3Summary(response.content)) {
|
|
3150
|
+
console.log(chalk.yellow('V3 agent finished after reading local files, but no final narrative summary was emitted.'));
|
|
3151
|
+
console.log(chalk.gray('Use /continue and ask for a written overview.'));
|
|
3152
|
+
}
|
|
2171
3153
|
}
|
|
2172
3154
|
else if (response.content) {
|
|
2173
|
-
if (!this.directPromptMode) {
|
|
3155
|
+
if (!v3UserReportPrinted && !this.directPromptMode) {
|
|
2174
3156
|
console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
|
|
2175
3157
|
}
|
|
2176
|
-
|
|
3158
|
+
if (!v3UserReportPrinted && this.shouldPrintV3FinalContent(response.content)) {
|
|
3159
|
+
console.log(response.content);
|
|
3160
|
+
}
|
|
3161
|
+
else if (!v3UserReportPrinted && this.isThinV3Summary(response.content)) {
|
|
3162
|
+
console.log('V3 agent workflow completed, but no final task summary was emitted.');
|
|
3163
|
+
}
|
|
3164
|
+
else if (!v3UserReportPrinted) {
|
|
3165
|
+
console.log(response.content);
|
|
3166
|
+
}
|
|
2177
3167
|
}
|
|
2178
3168
|
else {
|
|
2179
3169
|
if (!this.directPromptMode) {
|
|
@@ -2285,11 +3275,11 @@ export class ChatCommand {
|
|
|
2285
3275
|
this.printAgentRunSummary(this.lastAgentRunOutcome, executorSucceeded, changedFileCount);
|
|
2286
3276
|
}
|
|
2287
3277
|
this.messages.push({ role: 'assistant', content: response.content || 'V3 agent workflow completed.' });
|
|
2288
|
-
|
|
3278
|
+
stopWorkspaceSync();
|
|
2289
3279
|
return true;
|
|
2290
3280
|
}
|
|
2291
3281
|
catch (error) {
|
|
2292
|
-
|
|
3282
|
+
stopWorkspaceSync();
|
|
2293
3283
|
if (!this.api.hasAgentWorkspaceOutput(workspaceContext)) {
|
|
2294
3284
|
const recovered = await this.tryRecoverV3ServiceAndRetry(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
|
|
2295
3285
|
if (recovered) {
|
|
@@ -2592,9 +3582,15 @@ export class ChatCommand {
|
|
|
2592
3582
|
this.syncInteractiveModeModel('agent');
|
|
2593
3583
|
console.log(chalk.gray('Agent mode re-enabled for continuation.'));
|
|
2594
3584
|
}
|
|
2595
|
-
await this.runAgentTurn(followUp);
|
|
3585
|
+
await this.runAgentTurn(followUp, { skipLocalLoop: true, forceV3: true });
|
|
2596
3586
|
continue;
|
|
2597
3587
|
}
|
|
3588
|
+
if (/^(?:\/logout|logout)$/i.test(trimmed)) {
|
|
3589
|
+
const { logout } = await import('./auth.js');
|
|
3590
|
+
await logout();
|
|
3591
|
+
console.log(chalk.gray('Session ended.'));
|
|
3592
|
+
break;
|
|
3593
|
+
}
|
|
2598
3594
|
if (trimmed === '/save') {
|
|
2599
3595
|
this.saveSession();
|
|
2600
3596
|
console.log(chalk.green('Session saved.'));
|
|
@@ -2681,6 +3677,9 @@ export class ChatCommand {
|
|
|
2681
3677
|
console.log(chalk.gray(' Pending: ') + chalk.yellow(unfinishedList.join(', ')) + more);
|
|
2682
3678
|
}
|
|
2683
3679
|
}
|
|
3680
|
+
else if (!executorSucceeded) {
|
|
3681
|
+
console.log(chalk.gray(' The detailed overview is printed above when available.'));
|
|
3682
|
+
}
|
|
2684
3683
|
if (typeof outcome.qualityScore === 'number') {
|
|
2685
3684
|
const score = outcome.qualityScore.toFixed(1);
|
|
2686
3685
|
const colour = outcome.qualityScore >= 70 ? chalk.green : outcome.qualityScore >= 30 ? chalk.yellow : chalk.red;
|
|
@@ -2769,7 +3768,10 @@ export class ChatCommand {
|
|
|
2769
3768
|
const missingLine = o.qualityMissing.length > 0
|
|
2770
3769
|
? `\nMissing pieces: ${o.qualityMissing.slice(0, 6).join(', ')}.`
|
|
2771
3770
|
: '';
|
|
2772
|
-
return `Continue the previous agent run from the current workspace state without re-doing already-completed work.${taskList}${blockerLine}${missingLine}
|
|
3771
|
+
return `Continue the previous agent run from the current workspace state without re-doing already-completed work.${taskList}${blockerLine}${missingLine}
|
|
3772
|
+
Original request was: ${o.prompt}
|
|
3773
|
+
|
|
3774
|
+
Now implement the missing fixes by editing the local workspace files. Use write/edit tools to apply the required code and HTML changes so the project runs.`;
|
|
2773
3775
|
}
|
|
2774
3776
|
/**
|
|
2775
3777
|
* Re-print the last agent run summary, or guide the user when there isn't one.
|
|
@@ -2857,6 +3859,7 @@ export class ChatCommand {
|
|
|
2857
3859
|
});
|
|
2858
3860
|
}
|
|
2859
3861
|
buildAgentSystemPrompt() {
|
|
3862
|
+
const runtime = this.getRuntimeEnvironmentContext();
|
|
2860
3863
|
const toolCatalog = AgenticTools.getToolDefinitions()
|
|
2861
3864
|
.map((tool) => {
|
|
2862
3865
|
const params = tool.parameters
|
|
@@ -2868,6 +3871,7 @@ export class ChatCommand {
|
|
|
2868
3871
|
return [
|
|
2869
3872
|
'Vigthoria CLI agent operating contract.',
|
|
2870
3873
|
`You are operating inside the project root: ${this.currentProjectPath}`,
|
|
3874
|
+
`Execution platform: ${runtime.platform} (${runtime.osPlatform}). Machine scope: ${runtime.machineScope}.`,
|
|
2871
3875
|
`You are operating on the user's LOCAL machine. This CLI is not a server-only runtime.`,
|
|
2872
3876
|
`All file reads/writes and command execution must target the local project/workspace path unless the user explicitly requests remote/server execution.`,
|
|
2873
3877
|
'For command execution: ask for confirmation before risky commands; never redirect normal local work to server paths.',
|
|
@@ -2900,10 +3904,12 @@ export class ChatCommand {
|
|
|
2900
3904
|
].join('\n');
|
|
2901
3905
|
}
|
|
2902
3906
|
buildScopedUserPrompt(prompt) {
|
|
3907
|
+
const runtime = this.getRuntimeEnvironmentContext();
|
|
2903
3908
|
return [
|
|
2904
3909
|
this.buildExecutionPrompt(prompt),
|
|
2905
3910
|
'',
|
|
2906
3911
|
`Project root: ${this.currentProjectPath}`,
|
|
3912
|
+
`Execution platform: ${runtime.platform}. Machine scope: ${runtime.machineScope}.`,
|
|
2907
3913
|
'Stay within this project root unless the user explicitly expands scope.',
|
|
2908
3914
|
'Finish the request and stop once it is complete.',
|
|
2909
3915
|
].join('\n');
|
|
@@ -2965,7 +3971,14 @@ export class ChatCommand {
|
|
|
2965
3971
|
return true;
|
|
2966
3972
|
}
|
|
2967
3973
|
const extension = path.extname(targetFile).toLowerCase();
|
|
2968
|
-
|
|
3974
|
+
if (extension === '')
|
|
3975
|
+
return true;
|
|
3976
|
+
// HTML/HTM files need V3 agent with the 35B model for quality rewrites (canvas,
|
|
3977
|
+
// styling, interactivity). Route them to tryV3AgentWorkflow instead of the
|
|
3978
|
+
// lightweight cloud-chat direct-file path.
|
|
3979
|
+
if (extension === '.html' || extension === '.htm')
|
|
3980
|
+
return true;
|
|
3981
|
+
return false;
|
|
2969
3982
|
}
|
|
2970
3983
|
shouldPreferLocalAgentLoop(prompt) {
|
|
2971
3984
|
if (this.shouldRequireV3AgentWorkflow(prompt)) {
|
|
@@ -2975,16 +3988,44 @@ export class ChatCommand {
|
|
|
2975
3988
|
if (forceV3) {
|
|
2976
3989
|
return false;
|
|
2977
3990
|
}
|
|
3991
|
+
if (this.isBrowserTaskPrompt(prompt)) {
|
|
3992
|
+
return false;
|
|
3993
|
+
}
|
|
2978
3994
|
const runtime = this.getRuntimeEnvironmentContext();
|
|
2979
|
-
|
|
3995
|
+
const forceRemoteAgent = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_PREFER_V3_AGENT_LOOP
|
|
3996
|
+
|| process.env.VIGTHORIA_FORCE_REMOTE_AGENT
|
|
3997
|
+
|| ''));
|
|
3998
|
+
// CLI sessions on a user's local machine must execute tools locally.
|
|
3999
|
+
// Remote V3 only sees a partial hydrated copy and cannot reach real paths.
|
|
4000
|
+
if (runtime.machineScope === 'local-machine') {
|
|
4001
|
+
if (forceRemoteAgent) {
|
|
4002
|
+
return false;
|
|
4003
|
+
}
|
|
4004
|
+
if (this.isImplementationPrompt(prompt)) {
|
|
4005
|
+
return true;
|
|
4006
|
+
}
|
|
4007
|
+
const preferLocalOptIn = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_PREFER_LOCAL_AGENT_LOOP || ''));
|
|
4008
|
+
// Read-only analysis on a Windows/macOS/Linux desktop is faster and more
|
|
4009
|
+
// reliable through V3 + client-side read tools than a blocking chat route.
|
|
4010
|
+
if (this.isAnalysisLookupPrompt(prompt) && !preferLocalOptIn) {
|
|
4011
|
+
return false;
|
|
4012
|
+
}
|
|
4013
|
+
return true;
|
|
4014
|
+
}
|
|
4015
|
+
// Server-bindable workspaces keep V3 as the default execution path.
|
|
4016
|
+
const preferLocalOptIn = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_PREFER_LOCAL_AGENT_LOOP || ''));
|
|
4017
|
+
if (!preferLocalOptIn) {
|
|
4018
|
+
return false;
|
|
4019
|
+
}
|
|
4020
|
+
if (this.directPromptMode && this.isRepoGroundedPrompt(prompt)) {
|
|
4021
|
+
return false;
|
|
4022
|
+
}
|
|
2980
4023
|
if (!runtime.serverBindableWorkspace) {
|
|
2981
4024
|
return true;
|
|
2982
4025
|
}
|
|
2983
|
-
// Interactive sessions should prioritize local edits unless V3 is explicitly forced.
|
|
2984
4026
|
if (!this.directPromptMode) {
|
|
2985
4027
|
return true;
|
|
2986
4028
|
}
|
|
2987
|
-
// For direct prompts on server-bindable Linux roots, keep V3-first behavior.
|
|
2988
4029
|
return runtime.platform !== 'linux';
|
|
2989
4030
|
}
|
|
2990
4031
|
getRuntimeEnvironmentContext() {
|
|
@@ -3026,7 +4067,7 @@ export class ChatCommand {
|
|
|
3026
4067
|
if (!projectPath || !path.isAbsolute(projectPath) || !fs.existsSync(projectPath)) {
|
|
3027
4068
|
return false;
|
|
3028
4069
|
}
|
|
3029
|
-
const configuredRoots = (process.env.VIGTHORIA_SERVER_WORKSPACE_ROOTS || '/var/www/vigthoria-user-workspaces,/var/lib/vigthoria-workspaces')
|
|
4070
|
+
const configuredRoots = (process.env.VIGTHORIA_SERVER_WORKSPACE_ROOTS || '/var/www,/var/www/vigthoria-user-workspaces,/var/lib/vigthoria-workspaces')
|
|
3030
4071
|
.split(',')
|
|
3031
4072
|
.map((entry) => entry.trim())
|
|
3032
4073
|
.filter(Boolean);
|
|
@@ -3325,7 +4366,11 @@ export class ChatCommand {
|
|
|
3325
4366
|
* substantive answer.
|
|
3326
4367
|
*/
|
|
3327
4368
|
sanitizeDirectModeOutput(text) {
|
|
3328
|
-
let cleaned = text;
|
|
4369
|
+
let cleaned = this.stripHiddenThoughtBlocks(text);
|
|
4370
|
+
cleaned = cleaned
|
|
4371
|
+
.replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
|
|
4372
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
|
4373
|
+
.replace(/<\|(?:mask_start|mask_end)\|>/g, '');
|
|
3329
4374
|
// ── Phase 1: Strip entire tool-output blocks ──
|
|
3330
4375
|
// Matches "Tool <name> succeeded/FAILED." through the next blank line,
|
|
3331
4376
|
// next tool header, or end-of-string. The DOTALL-like [\s\S]*? is
|
|
@@ -3398,7 +4443,7 @@ export class ChatCommand {
|
|
|
3398
4443
|
cleaned = kept.join('\n\n');
|
|
3399
4444
|
// Collapse multiple blank lines
|
|
3400
4445
|
cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim();
|
|
3401
|
-
return cleaned || text;
|
|
4446
|
+
return cleaned || this.stripHiddenThoughtBlocks(text);
|
|
3402
4447
|
}
|
|
3403
4448
|
isDirectModeFollowUpQuestion(text) {
|
|
3404
4449
|
return /^(would you like me|do you want me|which aspect|what aspect|can you clarify|could you clarify|should i focus on|i will follow|i understand|i('ll| will) adhere|provide your|waiting for)/i.test(text.trim());
|
|
@@ -3480,6 +4525,57 @@ export class ChatCommand {
|
|
|
3480
4525
|
.trim();
|
|
3481
4526
|
return normalized || null;
|
|
3482
4527
|
}
|
|
4528
|
+
async tryLocalToolFallback(call, result) {
|
|
4529
|
+
if (result.success || !this.tools) {
|
|
4530
|
+
return result;
|
|
4531
|
+
}
|
|
4532
|
+
const runtime = this.getRuntimeEnvironmentContext();
|
|
4533
|
+
if (runtime.machineScope !== 'local-machine') {
|
|
4534
|
+
return result;
|
|
4535
|
+
}
|
|
4536
|
+
if (call.tool === 'read_file' && call.args.path) {
|
|
4537
|
+
const parent = path.dirname(call.args.path);
|
|
4538
|
+
const listPath = parent === '.' || parent === '' ? '.' : parent;
|
|
4539
|
+
const listResult = await this.tools.execute({
|
|
4540
|
+
tool: 'list_dir',
|
|
4541
|
+
args: { path: listPath },
|
|
4542
|
+
});
|
|
4543
|
+
if (listResult.success && listResult.output) {
|
|
4544
|
+
return {
|
|
4545
|
+
...result,
|
|
4546
|
+
suggestion: `File not found at "${call.args.path}". Parent directory "${listPath}" contains:\n${listResult.output}`,
|
|
4547
|
+
};
|
|
4548
|
+
}
|
|
4549
|
+
const baseName = path.basename(call.args.path);
|
|
4550
|
+
const globResult = await this.tools.execute({
|
|
4551
|
+
tool: 'glob',
|
|
4552
|
+
args: { pattern: `**/${baseName}` },
|
|
4553
|
+
});
|
|
4554
|
+
if (globResult.success && globResult.output?.trim()) {
|
|
4555
|
+
return {
|
|
4556
|
+
...result,
|
|
4557
|
+
suggestion: `File not found at "${call.args.path}". Matching paths:\n${globResult.output}`,
|
|
4558
|
+
};
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
if (call.tool === 'list_dir') {
|
|
4562
|
+
const listPath = call.args.path || '.';
|
|
4563
|
+
const command = runtime.platform === 'windows'
|
|
4564
|
+
? `dir /b "${path.join(this.currentProjectPath, listPath)}"`
|
|
4565
|
+
: `ls -la "${path.join(this.currentProjectPath, listPath)}"`;
|
|
4566
|
+
const shellResult = await this.tools.execute({
|
|
4567
|
+
tool: 'bash',
|
|
4568
|
+
args: { command, cwd: this.currentProjectPath },
|
|
4569
|
+
});
|
|
4570
|
+
if (shellResult.success && shellResult.output) {
|
|
4571
|
+
return {
|
|
4572
|
+
...result,
|
|
4573
|
+
suggestion: `list_dir failed for "${listPath}". Terminal listing:\n${shellResult.output}`,
|
|
4574
|
+
};
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
return result;
|
|
4578
|
+
}
|
|
3483
4579
|
async executeToolCalls(toolCalls) {
|
|
3484
4580
|
if (!this.tools) {
|
|
3485
4581
|
throw new Error('Agent tools are not initialized.');
|
|
@@ -3489,10 +4585,15 @@ export class ChatCommand {
|
|
|
3489
4585
|
const verbose = !this.jsonOutput;
|
|
3490
4586
|
for (const call of toolCalls) {
|
|
3491
4587
|
if (verbose) {
|
|
3492
|
-
|
|
4588
|
+
const target = call.args.path || call.args.pattern || call.args.command || '';
|
|
4589
|
+
const detail = target ? chalk.gray(` → ${String(target).replace(/\\/g, '/')}`) : '';
|
|
4590
|
+
console.log(chalk.cyan(`⚙ Executing: ${call.tool}`) + detail);
|
|
3493
4591
|
}
|
|
3494
4592
|
getBridgeClient()?.emitToolCall({ tool: call.tool, args: call.args });
|
|
3495
4593
|
let result = await this.tools.execute(call);
|
|
4594
|
+
if (!result.success) {
|
|
4595
|
+
result = await this.tryLocalToolFallback(call, result);
|
|
4596
|
+
}
|
|
3496
4597
|
// Phase 2: If a search tool failed (search_failed), retry with alternate approach
|
|
3497
4598
|
const searchStatus = result.metadata?.searchStatus;
|
|
3498
4599
|
if (call.tool === 'grep' && searchStatus === 'search_failed') {
|