vigthoria-cli 1.6.41 → 1.6.43
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 +19 -0
- package/dist/commands/chat.js +190 -9
- package/dist/utils/tools.js +14 -6
- package/package.json +1 -1
package/dist/commands/chat.d.ts
CHANGED
|
@@ -60,6 +60,13 @@ export declare class ChatCommand {
|
|
|
60
60
|
* file scanning, or infrastructure changes.
|
|
61
61
|
*/
|
|
62
62
|
private isOperatorDirectAnswerable;
|
|
63
|
+
/**
|
|
64
|
+
* Returns true when the prompt references code artefacts, file paths,
|
|
65
|
+
* or analysis verbs that require tool-backed file reading to answer
|
|
66
|
+
* correctly. Used to route operator prompts through the agent loop
|
|
67
|
+
* instead of the toolless BMAD/direct-answer path.
|
|
68
|
+
*/
|
|
69
|
+
private isRepoGroundedPrompt;
|
|
63
70
|
private inferAgentTaskType;
|
|
64
71
|
private buildTaskShapingInstructions;
|
|
65
72
|
private buildExecutionPrompt;
|
|
@@ -124,10 +131,22 @@ export declare class ChatCommand {
|
|
|
124
131
|
private isProtectedFileReferenceSafe;
|
|
125
132
|
private isProtectedFileReference;
|
|
126
133
|
private buildContinuationPrompt;
|
|
134
|
+
/**
|
|
135
|
+
* Extract Key* / event-handler identifiers from tool results grouped by
|
|
136
|
+
* file, then compute which identifiers actually overlap across files.
|
|
137
|
+
* Returns a formatted hint string, or empty string if not applicable.
|
|
138
|
+
*/
|
|
139
|
+
private computeCrossFileKeyEvidence;
|
|
127
140
|
private extractToolCalls;
|
|
128
141
|
private parseToolPayload;
|
|
129
142
|
private stripToolPayloads;
|
|
130
143
|
private extractFinalFileContent;
|
|
144
|
+
/**
|
|
145
|
+
* Synthesize a best-effort answer from tool evidence already collected
|
|
146
|
+
* in the message history. Used as a fallback when the model API fails
|
|
147
|
+
* on a continuation turn after evidence was gathered.
|
|
148
|
+
*/
|
|
149
|
+
private synthesizeEvidenceFromHistory;
|
|
131
150
|
private resolveDirectModeCompletion;
|
|
132
151
|
private isDirectModeFollowUpQuestion;
|
|
133
152
|
private buildLocalAnalysisFallback;
|
package/dist/commands/chat.js
CHANGED
|
@@ -216,11 +216,38 @@ class ChatCommand {
|
|
|
216
216
|
*/
|
|
217
217
|
isOperatorDirectAnswerable(prompt) {
|
|
218
218
|
const trimmed = prompt.trim();
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
// ── Exclusion guard: any prompt that references code artefacts,
|
|
220
|
+
// file paths, or analysis verbs MUST go through a tool-backed path
|
|
221
|
+
// (BMAD or agent loop), never the toolless direct-answer shortcut.
|
|
222
|
+
const repoGrounded = /\b(src\/|\.js\b|\.ts\b|\.py\b|\.jsx\b|\.tsx\b|\.css\b|\.html\b|\.json\b|\.yaml\b|\.yml\b)/i.test(trimmed)
|
|
223
|
+
|| /\b(file|folder|directory|module|class|function|method|variable|handler|listener|binding|conflict|bug|issue|error)\b/i.test(trimmed)
|
|
224
|
+
|| /\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(trimmed)
|
|
225
|
+
|| /\b(Camera|InputManager|keydown|KeyS|KeyA|KeyW|stopPropagation|addEventListener|handleKeyDown)\b/.test(trimmed)
|
|
226
|
+
|| /\/[a-zA-Z]/.test(trimmed) // slash-prefixed paths
|
|
227
|
+
|| /[A-Z][a-z]+\.[a-z_][a-zA-Z]+/.test(trimmed) // ClassName.methodName
|
|
228
|
+
|| /\b(key\s*code|keydown|keyup|key\s*bind|event\s*listener|event\s*handler|addEventListener|handleKey|onKey)\b/i.test(trimmed);
|
|
229
|
+
if (repoGrounded)
|
|
230
|
+
return false;
|
|
231
|
+
// Only truly trivial prompts are direct-answerable: short echo/smoke
|
|
232
|
+
// prompts and factual questions with no code or repo reference.
|
|
233
|
+
const isTrivialEcho = trimmed.length < 120
|
|
234
|
+
&& /^(reply with|say|echo|respond with|return|ping|hello|hi|test|status|what is \d|how much is)\b/i.test(trimmed);
|
|
235
|
+
return isTrivialEcho;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Returns true when the prompt references code artefacts, file paths,
|
|
239
|
+
* or analysis verbs that require tool-backed file reading to answer
|
|
240
|
+
* correctly. Used to route operator prompts through the agent loop
|
|
241
|
+
* instead of the toolless BMAD/direct-answer path.
|
|
242
|
+
*/
|
|
243
|
+
isRepoGroundedPrompt(prompt) {
|
|
244
|
+
const trimmed = prompt.trim();
|
|
245
|
+
return /\b(src\/|\.js\b|\.ts\b|\.py\b|\.jsx\b|\.tsx\b|\.css\b|\.html\b|\.json\b)/i.test(trimmed)
|
|
246
|
+
|| /\b(file|folder|module|class|function|method|handler|listener|binding|conflict|bug|error)\b/i.test(trimmed)
|
|
247
|
+
|| /\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(trimmed)
|
|
248
|
+
|| /\/[a-zA-Z]/.test(trimmed)
|
|
249
|
+
|| /[A-Z][a-z]+\.[a-z_][a-zA-Z]+/.test(trimmed) // ClassName.methodName patterns
|
|
250
|
+
|| /\b(key\s*code|keydown|keyup|key\s*bind|event\s*listener|event\s*handler|addEventListener|handleKey|onKey)\b/i.test(trimmed);
|
|
224
251
|
}
|
|
225
252
|
inferAgentTaskType(prompt) {
|
|
226
253
|
if (this.isDiagnosticPrompt(prompt))
|
|
@@ -798,6 +825,16 @@ class ChatCommand {
|
|
|
798
825
|
await this.runOperatorDirectAnswer(prompt);
|
|
799
826
|
return;
|
|
800
827
|
}
|
|
828
|
+
// ── Repo-grounded operator path: use the agent loop with tools ──
|
|
829
|
+
// Prompts that reference files, code symbols, or analysis verbs need
|
|
830
|
+
// tool access to read actual file contents. Route these through the
|
|
831
|
+
// local agent loop (which has read_file, grep, etc.) instead of the
|
|
832
|
+
// toolless BMAD SSE workflow which cannot read files and will fabricate.
|
|
833
|
+
if (this.isRepoGroundedPrompt(prompt) && this.tools) {
|
|
834
|
+
this.operatorMode = true;
|
|
835
|
+
await this.runLocalAgentLoop(prompt);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
801
838
|
(0, bridge_client_js_1.getBridgeClient)()?.emitPrompt({ prompt, mode: 'operator', model: this.currentModel });
|
|
802
839
|
const runtimeContext = await this.getPromptRuntimeContext(prompt);
|
|
803
840
|
const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking like an operator...', spinner: 'clock' }).start();
|
|
@@ -1096,10 +1133,52 @@ class ChatCommand {
|
|
|
1096
1133
|
const maxTurns = 10;
|
|
1097
1134
|
for (let turn = 0; turn < maxTurns; turn += 1) {
|
|
1098
1135
|
const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: turn === 0 ? 'Planning...' : 'Continuing...', spinner: 'clock' }).start();
|
|
1136
|
+
let response;
|
|
1137
|
+
try {
|
|
1138
|
+
response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
|
|
1139
|
+
}
|
|
1140
|
+
catch (firstErr) {
|
|
1141
|
+
// If we already gathered evidence and the model API fails on a
|
|
1142
|
+
// continuation turn, retry once after a short delay.
|
|
1143
|
+
if (turn > 0 && this.agentToolEvidence.discovery > 0) {
|
|
1144
|
+
this.logger.debug('Agent continuation API call failed, retrying once...');
|
|
1145
|
+
try {
|
|
1146
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1147
|
+
response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
|
|
1148
|
+
}
|
|
1149
|
+
catch (retryErr) {
|
|
1150
|
+
// Retry also failed — synthesize an answer from evidence
|
|
1151
|
+
if (spinner)
|
|
1152
|
+
spinner.stop();
|
|
1153
|
+
const evidenceSummary = this.synthesizeEvidenceFromHistory();
|
|
1154
|
+
if (evidenceSummary) {
|
|
1155
|
+
const fallbackContent = `[Agent recovered from backend failure — answer synthesized from gathered evidence]\n\n${evidenceSummary}`;
|
|
1156
|
+
if (this.jsonOutput) {
|
|
1157
|
+
console.log(JSON.stringify({
|
|
1158
|
+
success: true,
|
|
1159
|
+
mode: 'agent',
|
|
1160
|
+
model: this.currentModel,
|
|
1161
|
+
partial: true,
|
|
1162
|
+
content: fallbackContent,
|
|
1163
|
+
metadata: { executionPath: 'local-agent-loop', recovered: true },
|
|
1164
|
+
}, null, 2));
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
console.log(fallbackContent);
|
|
1168
|
+
}
|
|
1169
|
+
this.saveSession();
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
throw retryErr;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
throw firstErr;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (spinner)
|
|
1180
|
+
spinner.stop();
|
|
1099
1181
|
try {
|
|
1100
|
-
const response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
|
|
1101
|
-
if (spinner)
|
|
1102
|
-
spinner.stop();
|
|
1103
1182
|
const assistantMessage = response.message || '';
|
|
1104
1183
|
this.messages.push({ role: 'assistant', content: assistantMessage });
|
|
1105
1184
|
const toolCalls = this.extractToolCalls(assistantMessage);
|
|
@@ -1896,6 +1975,11 @@ class ChatCommand {
|
|
|
1896
1975
|
if (searchFailed > 0) {
|
|
1897
1976
|
evidenceLines.push(`Warning: ${searchFailed} search tool call(s) failed. Do not treat failed searches as evidence that something is missing.`);
|
|
1898
1977
|
}
|
|
1978
|
+
// Cross-file overlap: extract Key* identifiers per file from tool results
|
|
1979
|
+
const crossFileEvidence = this.computeCrossFileKeyEvidence();
|
|
1980
|
+
if (crossFileEvidence) {
|
|
1981
|
+
evidenceLines.push(crossFileEvidence);
|
|
1982
|
+
}
|
|
1899
1983
|
return [
|
|
1900
1984
|
`Tool results received for direct mode step ${this.directToolContinuationCount + 1}.`,
|
|
1901
1985
|
`Original user request: ${this.lastActionableUserInput}`,
|
|
@@ -1912,6 +1996,75 @@ class ChatCommand {
|
|
|
1912
1996
|
'Do not ask follow-up questions or drift into unrelated tasks.',
|
|
1913
1997
|
].join('\n');
|
|
1914
1998
|
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Extract Key* / event-handler identifiers from tool results grouped by
|
|
2001
|
+
* file, then compute which identifiers actually overlap across files.
|
|
2002
|
+
* Returns a formatted hint string, or empty string if not applicable.
|
|
2003
|
+
*/
|
|
2004
|
+
computeCrossFileKeyEvidence() {
|
|
2005
|
+
const fileKeyMap = new Map();
|
|
2006
|
+
// Match standalone Key* codes like KeyA, KeyS, KeyW etc.
|
|
2007
|
+
// Exclude false positives from method names like handleKeyDown, KeyboardEvent
|
|
2008
|
+
const keyPattern = /(?<![a-z])Key[A-Z](?![a-z])/g;
|
|
2009
|
+
let currentFile = '';
|
|
2010
|
+
for (const msg of this.messages) {
|
|
2011
|
+
if (msg.role !== 'system')
|
|
2012
|
+
continue;
|
|
2013
|
+
// Detect which file a tool result is about from "File: xxx" tag
|
|
2014
|
+
const fileTag = msg.content.match(/^Tool (?:read_file|grep) (?:succeeded|FAILED)\.\nFile: (.+)/m);
|
|
2015
|
+
if (fileTag) {
|
|
2016
|
+
currentFile = fileTag[1].trim();
|
|
2017
|
+
}
|
|
2018
|
+
if (!currentFile)
|
|
2019
|
+
continue;
|
|
2020
|
+
// Extract keys from code content, filtering out method name false positives
|
|
2021
|
+
const cleanedContent = msg.content
|
|
2022
|
+
.replace(/handleKeyDown|handleKeyUp|onKeyDown|onKeyUp|_onKeyDown|_onKeyUp|KeyboardEvent|keydown|keyup/gi, '___');
|
|
2023
|
+
const keys = cleanedContent.match(keyPattern);
|
|
2024
|
+
if (keys) {
|
|
2025
|
+
if (!fileKeyMap.has(currentFile)) {
|
|
2026
|
+
fileKeyMap.set(currentFile, new Set());
|
|
2027
|
+
}
|
|
2028
|
+
const keySet = fileKeyMap.get(currentFile);
|
|
2029
|
+
for (const k of keys)
|
|
2030
|
+
keySet.add(k);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
if (fileKeyMap.size < 2)
|
|
2034
|
+
return '';
|
|
2035
|
+
// Compute overlap
|
|
2036
|
+
const allFiles = [...fileKeyMap.keys()];
|
|
2037
|
+
const allKeys = new Set();
|
|
2038
|
+
for (const ks of fileKeyMap.values())
|
|
2039
|
+
for (const k of ks)
|
|
2040
|
+
allKeys.add(k);
|
|
2041
|
+
const overlapping = [];
|
|
2042
|
+
const unique = new Map();
|
|
2043
|
+
for (const key of allKeys) {
|
|
2044
|
+
const filesWithKey = allFiles.filter(f => fileKeyMap.get(f).has(key));
|
|
2045
|
+
if (filesWithKey.length > 1) {
|
|
2046
|
+
overlapping.push(key);
|
|
2047
|
+
}
|
|
2048
|
+
else {
|
|
2049
|
+
const file = filesWithKey[0];
|
|
2050
|
+
if (!unique.has(file))
|
|
2051
|
+
unique.set(file, []);
|
|
2052
|
+
unique.get(file).push(key);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
const lines = ['MANDATORY CROSS-FILE EVIDENCE (computed from actual tool output — this is ground truth):'];
|
|
2056
|
+
if (overlapping.length > 0) {
|
|
2057
|
+
lines.push(`CONFIRMED CONFLICTING keys (found in MULTIPLE files): ${overlapping.join(', ')}`);
|
|
2058
|
+
}
|
|
2059
|
+
else {
|
|
2060
|
+
lines.push('WARNING: No keys found in multiple files — there may be no actual key conflict.');
|
|
2061
|
+
}
|
|
2062
|
+
for (const [file, keys] of unique) {
|
|
2063
|
+
lines.push(`Keys found ONLY in ${file} (NOT in other files): ${keys.join(', ')}`);
|
|
2064
|
+
}
|
|
2065
|
+
lines.push('CONSTRAINT: Your answer MUST list ONLY the keys from the "CONFIRMED CONFLICTING" line above as conflicting. Keys listed as "ONLY in [file]" MUST NOT be reported as conflicts. This data was computed from the exact tool output and overrides any assumption.');
|
|
2066
|
+
return lines.join('\n');
|
|
2067
|
+
}
|
|
1915
2068
|
extractToolCalls(message) {
|
|
1916
2069
|
const calls = [];
|
|
1917
2070
|
const seen = new Set();
|
|
@@ -1973,6 +2126,30 @@ class ChatCommand {
|
|
|
1973
2126
|
}
|
|
1974
2127
|
return trimmed;
|
|
1975
2128
|
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Synthesize a best-effort answer from tool evidence already collected
|
|
2131
|
+
* in the message history. Used as a fallback when the model API fails
|
|
2132
|
+
* on a continuation turn after evidence was gathered.
|
|
2133
|
+
*/
|
|
2134
|
+
synthesizeEvidenceFromHistory() {
|
|
2135
|
+
const toolOutputs = [];
|
|
2136
|
+
for (const msg of this.messages) {
|
|
2137
|
+
if (msg.role === 'system' && msg.content.includes('Tool results')) {
|
|
2138
|
+
// Collect tool result summaries
|
|
2139
|
+
toolOutputs.push(msg.content);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
if (toolOutputs.length === 0)
|
|
2143
|
+
return '';
|
|
2144
|
+
// Extract file contents and grep results from tool outputs
|
|
2145
|
+
const evidence = ['Evidence gathered before backend failure:\n'];
|
|
2146
|
+
for (const output of toolOutputs) {
|
|
2147
|
+
// Truncate long outputs but keep the key evidence
|
|
2148
|
+
const trimmed = output.length > 3000 ? output.slice(0, 3000) + '\n[...truncated]' : output;
|
|
2149
|
+
evidence.push(trimmed);
|
|
2150
|
+
}
|
|
2151
|
+
return evidence.join('\n---\n');
|
|
2152
|
+
}
|
|
1976
2153
|
resolveDirectModeCompletion(prompt, visibleText) {
|
|
1977
2154
|
const normalized = (visibleText || '').trim();
|
|
1978
2155
|
if (normalized && !this.isDirectModeFollowUpQuestion(normalized)) {
|
|
@@ -2110,6 +2287,10 @@ class ChatCommand {
|
|
|
2110
2287
|
}
|
|
2111
2288
|
formatToolResult(call, result) {
|
|
2112
2289
|
const parts = [`Tool ${call.tool} ${result.success ? 'succeeded' : 'FAILED'}.`];
|
|
2290
|
+
// Include file path for read_file/grep so cross-file evidence can be computed
|
|
2291
|
+
if (call.args.path) {
|
|
2292
|
+
parts.push(`File: ${call.args.path}`);
|
|
2293
|
+
}
|
|
2113
2294
|
// Include search status classification for the agent to reason about
|
|
2114
2295
|
const searchStatus = result.metadata?.searchStatus;
|
|
2115
2296
|
if (searchStatus) {
|
|
@@ -2127,7 +2308,7 @@ class ChatCommand {
|
|
|
2127
2308
|
return parts.join('\n');
|
|
2128
2309
|
}
|
|
2129
2310
|
truncateText(text) {
|
|
2130
|
-
const maxLength =
|
|
2311
|
+
const maxLength = 12000;
|
|
2131
2312
|
if (text.length <= maxLength) {
|
|
2132
2313
|
return text;
|
|
2133
2314
|
}
|
package/dist/utils/tools.js
CHANGED
|
@@ -624,12 +624,20 @@ class AgenticTools {
|
|
|
624
624
|
}
|
|
625
625
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
626
626
|
const lines = content.split('\n');
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (
|
|
631
|
-
|
|
632
|
-
|
|
627
|
+
// Clamp start_line: API is 1-based; the model sometimes emits 0 or
|
|
628
|
+
// negative values. Silently clamp to valid range instead of failing.
|
|
629
|
+
let rawStart = args.start_line ? parseInt(args.start_line) : 1;
|
|
630
|
+
if (rawStart < 1)
|
|
631
|
+
rawStart = 1;
|
|
632
|
+
if (rawStart > lines.length)
|
|
633
|
+
rawStart = lines.length;
|
|
634
|
+
const startLine = rawStart - 1; // convert to 0-based
|
|
635
|
+
let rawEnd = args.end_line ? parseInt(args.end_line) : lines.length;
|
|
636
|
+
if (rawEnd < rawStart)
|
|
637
|
+
rawEnd = rawStart;
|
|
638
|
+
if (rawEnd > lines.length)
|
|
639
|
+
rawEnd = lines.length;
|
|
640
|
+
const endLine = rawEnd;
|
|
633
641
|
const selectedLines = lines.slice(startLine, Math.min(endLine, lines.length));
|
|
634
642
|
return {
|
|
635
643
|
success: true,
|