vigthoria-cli 1.6.42 → 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.
@@ -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;
@@ -216,11 +216,38 @@ class ChatCommand {
216
216
  */
217
217
  isOperatorDirectAnswerable(prompt) {
218
218
  const trimmed = prompt.trim();
219
- // Short prompts (under 200 chars) that do not contain action verbs
220
- // targeting infrastructure are almost always direct-answerable.
221
- const isShortNonAction = trimmed.length < 200
222
- && !/\b(deploy|provision|scale|migrate|rollback|restart|install|configure|create|build|scaffold|generate|set up|tear down|spin up|initialize|bootstrap)\b/i.test(trimmed);
223
- return this.isAnalysisLookupPrompt(trimmed) || isShortNonAction;
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 = 4000;
2311
+ const maxLength = 12000;
2131
2312
  if (text.length <= maxLength) {
2132
2313
  return text;
2133
2314
  }
@@ -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
- const startLine = args.start_line ? parseInt(args.start_line) - 1 : 0;
628
- const endLine = args.end_line ? parseInt(args.end_line) : lines.length;
629
- // Validate line numbers
630
- if (startLine < 0 || startLine >= lines.length) {
631
- return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Invalid start_line: ${args.start_line}. File has ${lines.length} lines.`);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.42",
3
+ "version": "1.6.43",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [