minovative-mind-cli 2.4.0 → 2.5.0

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.
@@ -16,15 +16,13 @@ export interface MetricCollector {
16
16
  recordSelfCorrection(): void;
17
17
  recordVerificationResult(passed: boolean): void;
18
18
  recordMatchTier(tier: 'exact' | 'normalized' | 'levenshtein' | 'none'): void;
19
- recordCacheHit(): void;
20
- recordCacheMiss(): void;
21
- recordInvestigationFailure(): void;
22
- recordToolFailure(toolName: string): void;
23
- recordModifyFailure(): void;
24
- recordWriteFailure(): void;
25
- recordCacheHit(cacheType: 'investigation' | 'read'): void;
26
- recordCacheMiss(cacheType: 'investigation' | 'read'): void;
19
+ recordCacheHit(cacheType?: 'investigation' | 'read'): void;
20
+ recordCacheMiss(cacheType?: 'investigation' | 'read'): void;
27
21
  recordCachePerformance(cacheType: 'investigation' | 'read', durationMs: number): void;
22
+ recordWriteFailure?(): void;
23
+ recordModifyFailure?(): void;
24
+ recordToolFailure?(toolName?: string): void;
25
+ recordInvestigationFailure?(): void;
28
26
  }
29
27
  export declare function setMetricCollector(collector: MetricCollector | null): void;
30
28
  export declare function getMetricCollector(): MetricCollector | null;
@@ -113,9 +113,6 @@ export class InvestigationAgentRunner {
113
113
  }
114
114
  currentMessage += `\n\nStart investigating to find relevant files within your assigned domains.`;
115
115
  let isFinished = false;
116
- const visitedToolCalls = new Set();
117
- let consecutiveDuplicates = 0;
118
- const MAX_CONSECUTIVE_DUPLICATES = 3;
119
116
  // Tool loop — mirrors the existing context agent loop in contextAgent.ts
120
117
  while (!crashed && !abortSignal.aborted && !isFinished) {
121
118
  this.pingHeartbeat();
@@ -143,35 +140,6 @@ export class InvestigationAgentRunner {
143
140
  this.pingHeartbeat();
144
141
  const args = call.args;
145
142
  const logPrefix = `[${this.agentLabel}]`;
146
- // Deduplicate identical tool calls
147
- const callSignature = `${call.name}:${JSON.stringify(args)}`;
148
- if (call.name !== 'finish_investigation' && visitedToolCalls.has(callSignature)) {
149
- consecutiveDuplicates++;
150
- if (consecutiveDuplicates >= MAX_CONSECUTIVE_DUPLICATES) {
151
- // Force-finish: the model is stuck in a loop
152
- const forceMsg = `Investigation auto-completed: the model repeated the same tool call ${MAX_CONSECUTIVE_DUPLICATES} times consecutively.`;
153
- debugLog(`InvestigationAgent [${this.agentLabel}]: ${forceMsg}`);
154
- if (onProgress)
155
- onProgress(`[${this.agentLabel}] ${forceMsg}`);
156
- if (!summary || summary === 'No relevant context found.') {
157
- summary = 'Investigation was auto-completed due to repeated duplicate tool calls. Review the gathered files for context.';
158
- }
159
- isFinished = true;
160
- success = relevantFiles.size > 0;
161
- break;
162
- }
163
- functionResponses.push({
164
- functionResponse: {
165
- name: call.name,
166
- response: {
167
- error: `DUPLICATE CALL BLOCKED (attempt ${consecutiveDuplicates}/${MAX_CONSECUTIVE_DUPLICATES}): You already executed this exact tool call. Do NOT retry it. Use the results you already have and call finish_investigation now, or try a DIFFERENT tool call with different parameters.`,
168
- },
169
- },
170
- });
171
- continue;
172
- }
173
- consecutiveDuplicates = 0;
174
- visitedToolCalls.add(callSignature);
175
143
  if (call.name === 'finish_investigation') {
176
144
  summary = args.summary || '';
177
145
  const filesToRead = args.relevantFiles || [];
@@ -27,7 +27,7 @@ import { buildDependencyGraph } from '../../utils/dependencyTracer.js';
27
27
  import { debugLog } from '../../utils/logger.js';
28
28
  // ─── Constants ───────────────────────────────────────────────────────
29
29
  /** Maximum total relevant files across all agents after merge. */
30
- const MAX_TOTAL_FILES = 15;
30
+ const MAX_TOTAL_FILES = 30;
31
31
  // ─── Investigation Orchestrator ──────────────────────────────────────
32
32
  export class InvestigationOrchestrator {
33
33
  /**
@@ -86,7 +86,7 @@ export declare class MessageBus {
86
86
  private readonly persistPath;
87
87
  private readonly workspaceRoot;
88
88
  /** Maximum semantic signals any single agent can post */
89
- static readonly MAX_SIGNALS_PER_AGENT = 20;
89
+ static readonly MAX_SIGNALS_PER_AGENT = 50;
90
90
  constructor(workspaceRoot: string, conversationId: string);
91
91
  /**
92
92
  * Records a tool execution into the activity log. Called by the scoped tool
@@ -38,7 +38,7 @@ export class MessageBus {
38
38
  persistPath;
39
39
  workspaceRoot;
40
40
  /** Maximum semantic signals any single agent can post */
41
- static MAX_SIGNALS_PER_AGENT = 20;
41
+ static MAX_SIGNALS_PER_AGENT = 50;
42
42
  constructor(workspaceRoot, conversationId) {
43
43
  this.workspaceRoot = workspaceRoot;
44
44
  const orchestrationDir = path.join(workspaceRoot, '.minovativemind', 'orchestration');
@@ -64,7 +64,7 @@ export class MessageBus {
64
64
  * @returns `true` if the signal was accepted, `false` if the agent hit the cap.
65
65
  */
66
66
  postSignal(signal) {
67
- const agentSignalCount = this.signals.filter(s => s.fromAgent === signal.fromAgent).length;
67
+ const agentSignalCount = this.signals.filter((s) => s.fromAgent === signal.fromAgent).length;
68
68
  if (agentSignalCount >= MessageBus.MAX_SIGNALS_PER_AGENT) {
69
69
  debugLog(`MessageBus: Agent ${signal.fromAgent} hit signal cap (${MessageBus.MAX_SIGNALS_PER_AGENT}). ` +
70
70
  `Dropping signal of type "${signal.type}".`);
@@ -85,12 +85,8 @@ export class MessageBus {
85
85
  getUnread(agentId) {
86
86
  const actCursor = this.activityCursors.get(agentId) ?? 0;
87
87
  const sigCursor = this.signalCursors.get(agentId) ?? 0;
88
- const activities = this.activityLog
89
- .slice(actCursor)
90
- .filter(e => e.agentId !== agentId);
91
- const signals = this.signals
92
- .slice(sigCursor)
93
- .filter(s => s.fromAgent !== agentId);
88
+ const activities = this.activityLog.slice(actCursor).filter((e) => e.agentId !== agentId);
89
+ const signals = this.signals.slice(sigCursor).filter((s) => s.fromAgent !== agentId);
94
90
  // Advance cursors to current end
95
91
  this.activityCursors.set(agentId, this.activityLog.length);
96
92
  this.signalCursors.set(agentId, this.signals.length);
@@ -111,7 +107,7 @@ export class MessageBus {
111
107
  * recovery — collecting partial progress before re-dispatch).
112
108
  */
113
109
  getAgentActivity(agentId) {
114
- return this.activityLog.filter(e => e.agentId === agentId);
110
+ return this.activityLog.filter((e) => e.agentId === agentId);
115
111
  }
116
112
  /**
117
113
  * Returns the total number of activity entries and signals in the bus.
@@ -131,11 +127,13 @@ export class MessageBus {
131
127
  static formatActivityEntries(entries) {
132
128
  if (entries.length === 0)
133
129
  return '';
134
- return entries.map(e => {
130
+ return entries
131
+ .map((e) => {
135
132
  const statusIcon = e.status === 'success' ? '✓' : '✗';
136
133
  const summary = e.resultSummary ? ` | ${e.resultSummary}` : '';
137
134
  return ` ${e.agentId} | ${e.tool.padEnd(14)} → ${e.target.padEnd(40)} | ${statusIcon} ${e.action}${summary}`;
138
- }).join('\n');
135
+ })
136
+ .join('\n');
139
137
  }
140
138
  /**
141
139
  * Formats semantic signals into a readable string for agent context injection.
@@ -143,7 +141,8 @@ export class MessageBus {
143
141
  static formatSignals(signals) {
144
142
  if (signals.length === 0)
145
143
  return '';
146
- return signals.map(s => {
144
+ return signals
145
+ .map((s) => {
147
146
  const tag = s.type.toUpperCase();
148
147
  switch (s.type) {
149
148
  case 'discovery':
@@ -157,7 +156,8 @@ export class MessageBus {
157
156
  default:
158
157
  return ` [SIGNAL from ${s.fromAgent}]: ${JSON.stringify(s)}`;
159
158
  }
160
- }).join('\n');
159
+ })
160
+ .join('\n');
161
161
  }
162
162
  // ─── Persistence ─────────────────────────────────────────────────
163
163
  /**
@@ -173,7 +173,7 @@ export class MessageBus {
173
173
  signalCursors: Object.fromEntries(this.signalCursors),
174
174
  };
175
175
  // Fire-and-forget — don't block the calling agent's tool loop
176
- atomicWriteFile(this.persistPath, JSON.stringify(snapshot)).catch(err => {
176
+ atomicWriteFile(this.persistPath, JSON.stringify(snapshot)).catch((err) => {
177
177
  debugLog(`MessageBus: Failed to persist to disk: ${err}`);
178
178
  });
179
179
  }
@@ -17,6 +17,7 @@
17
17
  * to prevent OOM on very large monorepos.
18
18
  */
19
19
  import { debugLog } from '../../utils/logger.js';
20
+ import { getMetricCollector } from '../metrics.js';
20
21
  // ─── Constants ───────────────────────────────────────────────────────
21
22
  /** Maximum total bytes of cached file content before LRU eviction kicks in. */
22
23
  const MAX_CACHE_BYTES = 5 * 1024 * 1024; // 5 MB
@@ -45,7 +46,6 @@ export class ReadCache {
45
46
  */
46
47
  has(filePath) {
47
48
  const found = this.cache.has(filePath);
48
- const { getMetricCollector } = require('./metrics.js');
49
49
  const collector = getMetricCollector();
50
50
  if (found) {
51
51
  this.hitCount++;
@@ -145,7 +145,8 @@ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus,
145
145
  else if (name === 'grep_search') {
146
146
  targetDesc = args.pattern;
147
147
  actionDesc = 'Searched';
148
- const matchCount = Array.isArray(result) ? result.length : 0;
148
+ const outputText = typeof result === 'object' && result?.output ? result.output : String(result || '');
149
+ const matchCount = (outputText.match(/\.\/[^:]+:\d+:/g) || []).length;
149
150
  resultSummary = `${matchCount} matches`;
150
151
  }
151
152
  return result;
@@ -63,11 +63,14 @@ export class SubAgentRunner {
63
63
  `${this.globalContext}\n` +
64
64
  `</reference_context>\n\n` +
65
65
  `<critical_guidelines>\n` +
66
- `1. You are ONE worker in a team. You MUST ONLY focus on your specific objective: "${this.intent}".\n` +
67
- `2. DO NOT attempt to fulfill the entire original user request in the reference context. Other agents are handling the other parts.\n` +
68
- `3. You are part of a parallelized system. Use 'post_message' to coordinate if you discover breaking changes.\n` +
69
- `4. When you have completed your objective, stop using tools and provide a final summary of your work.\n` +
70
- `5. If you encounter an insurmountable error, provide a summary of what went wrong so the orchestrator can re-assign or fix it.\n` +
66
+ `1. You are ONE worker in a team. Focus EXCLUSIVELY on your specific objective: "${this.intent}".\n` +
67
+ `2. DO NOT attempt to fulfill the entire original user request in the reference context. Other agents handle other tasks.\n` +
68
+ `3. EXECUTION WORKFLOW:\n` +
69
+ ` - Step 1 (Locate): Call 'grep_search' or 'read_file' ONCE to inspect the file you need to edit.\n` +
70
+ ` - Step 2 (Modify): Call 'modify_file' or 'write_file' to implement the required changes.\n` +
71
+ ` - Step 3 (Conclude): Immediately STOP using tools and return a text summary of your changes.\n` +
72
+ `4. Do NOT call 'grep_search' or 'run_command' repeatedly in a loop. Once you have file context or command results, proceed directly to modifying code or responding with your text summary.\n` +
73
+ `5. Use 'post_message' only if you discover breaking changes affecting other agents.\n` +
71
74
  `</critical_guidelines>`);
72
75
  }
73
76
  /**
@@ -87,12 +90,10 @@ export class SubAgentRunner {
87
90
  */
88
91
  async execute(signal) {
89
92
  return runWithAgentId(this.taskId, async () => {
90
- debugLog(`SubAgent [${this.taskId}]: Starting execution.`);
91
- this.lastHeartbeat = Date.now();
92
93
  let success = false;
93
94
  let finalSummary = '';
94
95
  let crashed = false;
95
- // Health Monitor Timer
96
+ // Health monitor interval: checks every 5s if the last tool execution/heartbeat stalled
96
97
  const healthMonitor = setInterval(() => {
97
98
  if (Date.now() - this.lastHeartbeat > SubAgentRunner.STALL_TIMEOUT_MS) {
98
99
  debugLog(`SubAgent [${this.taskId}]: STALL DETECTED. No heartbeat for ${SubAgentRunner.STALL_TIMEOUT_MS}ms.`);
@@ -103,14 +104,12 @@ export class SubAgentRunner {
103
104
  }, 5000);
104
105
  try {
105
106
  // Send initial prompt
106
- const prompt = `Begin execution for task: ${this.taskId}\nObjective: ${this.intent}\n\nYou must use tools to achieve this objective. Do not stop until the objective is fully complete.`;
107
+ const prompt = `Begin execution for task: ${this.taskId}\nObjective: ${this.intent}\n\nFollow the 3-step workflow: 1) inspect file -> 2) modify code -> 3) respond with text summary. Do not repeat search tools once results are returned.`;
107
108
  let turnResult = await this.chat.sendMessage(prompt, undefined, signal);
108
109
  this.updateUsage(turnResult);
109
110
  // Tool Loop
110
111
  const MAX_TURNS = Infinity;
111
112
  let turns = 0;
112
- // Anti-loop tracking
113
- const visitedToolCalls = new Set();
114
113
  while (turns < MAX_TURNS && !crashed && !signal.aborted) {
115
114
  this.pingHeartbeat();
116
115
  const calls = turnResult.response.functionCalls();
@@ -125,21 +124,6 @@ export class SubAgentRunner {
125
124
  for (const call of calls) {
126
125
  if (crashed || signal.aborted)
127
126
  break;
128
- // Anti-loop check: Hash the call to detect exact repetitions
129
- const callSignature = `${call.name}:${JSON.stringify(call.args)}`;
130
- if (visitedToolCalls.has(callSignature)) {
131
- debugLog(`SubAgent [${this.taskId}]: Detected identical tool call ${call.name}. Blocking to prevent loop.`);
132
- toolResponses.push({
133
- functionResponse: {
134
- name: call.name,
135
- response: {
136
- error: 'You have already made this exact tool call previously. Please review your context history or try a different action.',
137
- },
138
- },
139
- });
140
- continue;
141
- }
142
- visitedToolCalls.add(callSignature);
143
127
  this.pingHeartbeat();
144
128
  if (this.onProgress) {
145
129
  this.onProgress(`executing ${call.name}...`);
@@ -32,6 +32,14 @@ export declare function peekTurnUsage(): {
32
32
  remainingBalance: number | undefined;
33
33
  modelsUsed: Record<string, number>;
34
34
  };
35
+ export declare function accumulateTurnUsage(usage: {
36
+ promptTokens?: number;
37
+ candidatesTokens?: number;
38
+ cachedTokens?: number;
39
+ promptTokenCount?: number;
40
+ candidatesTokenCount?: number;
41
+ cachedContentTokenCount?: number;
42
+ }, modelName: string): void;
35
43
  /**
36
44
  * Client service interacting directly with the serverless Gemini proxy endpoint.
37
45
  * Ensures authorization via Firebase token passing and parses streamed content.
@@ -1,4 +1,5 @@
1
1
  import { debugLog } from '../utils/logger.js';
2
+ import { getMetricCollector } from './metrics.js';
2
3
  /**
3
4
  * ============================================================================
4
5
  * PROXY CLIENT SERVICE
@@ -58,6 +59,16 @@ export function getAndResetTurnUsage() {
58
59
  export function peekTurnUsage() {
59
60
  return { ...globalSessionAccumulatedUsage };
60
61
  }
62
+ export function accumulateTurnUsage(usage, modelName) {
63
+ const pTokens = usage.promptTokens ?? usage.promptTokenCount ?? 0;
64
+ const cTokens = usage.candidatesTokens ?? usage.candidatesTokenCount ?? 0;
65
+ const cachedTokens = usage.cachedTokens ?? usage.cachedContentTokenCount ?? 0;
66
+ globalSessionAccumulatedUsage.promptTokens += pTokens;
67
+ globalSessionAccumulatedUsage.candidatesTokens += cTokens;
68
+ globalSessionAccumulatedUsage.cachedTokens += cachedTokens;
69
+ globalSessionAccumulatedUsage.totalTokenCount += pTokens + cachedTokens + cTokens;
70
+ globalSessionAccumulatedUsage.modelsUsed[modelName] = (globalSessionAccumulatedUsage.modelsUsed[modelName] || 0) + 1;
71
+ }
61
72
  /**
62
73
  * Client service interacting directly with the serverless Gemini proxy endpoint.
63
74
  * Ensures authorization via Firebase token passing and parses streamed content.
@@ -183,6 +194,12 @@ export class ProxyClient {
183
194
  else if (data.type === 'done') {
184
195
  if (data.usage) {
185
196
  usageMetadata = data.usage;
197
+ const collector = getMetricCollector();
198
+ collector?.accumulateUsage({
199
+ promptTokens: data.usage.promptTokens || 0,
200
+ candidatesTokens: data.usage.candidatesTokens || 0,
201
+ cachedTokens: data.usage.cachedTokens || 0
202
+ });
186
203
  globalSessionAccumulatedUsage.promptTokens += data.usage.promptTokens || 0;
187
204
  globalSessionAccumulatedUsage.candidatesTokens += data.usage.candidatesTokens || 0;
188
205
  globalSessionAccumulatedUsage.cachedTokens += data.usage.cachedTokens || 0;
@@ -11,7 +11,7 @@ export async function detectVerificationCommand(workspaceRoot) {
11
11
  { name: 'pnpm-lock.yaml', prefix: 'pnpm run' },
12
12
  { name: 'yarn.lock', prefix: 'yarn run' },
13
13
  { name: 'bun.lockb', prefix: 'bun run' },
14
- { name: 'bun.lock', prefix: 'bun run' }
14
+ { name: 'bun.lock', prefix: 'bun run' },
15
15
  ];
16
16
  for (const lf of lockFiles) {
17
17
  try {
@@ -66,14 +66,20 @@ export async function detectVerificationCommand(workspaceRoot) {
66
66
  catch { }
67
67
  try {
68
68
  await fs.access(path.join(workspaceRoot, 'build.gradle'));
69
- return (await fs.access(path.join(workspaceRoot, 'gradlew')).then(() => true).catch(() => false))
69
+ return (await fs
70
+ .access(path.join(workspaceRoot, 'gradlew'))
71
+ .then(() => true)
72
+ .catch(() => false))
70
73
  ? './gradlew classes testClasses'
71
74
  : 'gradle classes testClasses';
72
75
  }
73
76
  catch { }
74
77
  try {
75
78
  await fs.access(path.join(workspaceRoot, 'build.gradle.kts'));
76
- return (await fs.access(path.join(workspaceRoot, 'gradlew')).then(() => true).catch(() => false))
79
+ return (await fs
80
+ .access(path.join(workspaceRoot, 'gradlew'))
81
+ .then(() => true)
82
+ .catch(() => false))
77
83
  ? './gradlew classes testClasses'
78
84
  : 'gradle classes testClasses';
79
85
  }
@@ -126,7 +132,10 @@ export async function detectVerificationCommand(workspaceRoot) {
126
132
  }
127
133
  try {
128
134
  await fs.access(path.join(workspaceRoot, 'Gemfile'));
129
- return (await fs.access(path.join(workspaceRoot, 'spec')).then(() => true).catch(() => false))
135
+ return (await fs
136
+ .access(path.join(workspaceRoot, 'spec'))
137
+ .then(() => true)
138
+ .catch(() => false))
130
139
  ? 'bundle exec rspec'
131
140
  : 'bundle exec rubocop';
132
141
  }
@@ -137,7 +146,7 @@ export async function runVerification(workspaceRoot, abortSignal) {
137
146
  const command = await detectVerificationCommand(workspaceRoot);
138
147
  if (!command)
139
148
  return null;
140
- const MAX_VERIFY_OUTPUT = 20_000; // 20KB cap on verification output
149
+ const MAX_VERIFY_OUTPUT = 50_000; // 50KB cap on verification output
141
150
  debugLog(`Running project-level verification command: ${command}`);
142
151
  try {
143
152
  const { stdout, stderr } = await execAsync(command, {
@@ -225,7 +234,7 @@ ${result.errors.join('\n')}
225
234
 
226
235
  Please fix these errors using the modify_file tool.`;
227
236
  }
228
- import { auditFilePerformance, formatAuditForModel, formatAuditForTerminal, isAuditableFile } from '../utils/performanceAuditor.js';
237
+ import { auditFilePerformance, formatAuditForModel, formatAuditForTerminal, isAuditableFile, } from '../utils/performanceAuditor.js';
229
238
  export async function verifyChangedFiles(workspaceRoot, filePaths, abortSignal) {
230
239
  const errors = [];
231
240
  const perfAudits = [];
@@ -23,3 +23,7 @@ export declare const GEMINI_MODELS: {
23
23
  export declare const DEFAULT_MODEL: "auto";
24
24
  /** Maximum tokens the model can output per response. */
25
25
  export declare const MAX_OUTPUT_TOKENS = 60000;
26
+ /**
27
+ * Checks if BYOK is currently enabled for the user.
28
+ */
29
+ export declare function isByokEnabled(): Promise<boolean>;
@@ -23,3 +23,11 @@ export const GEMINI_MODELS = {
23
23
  export const DEFAULT_MODEL = GEMINI_MODELS.AUTO;
24
24
  /** Maximum tokens the model can output per response. */
25
25
  export const MAX_OUTPUT_TOKENS = 60_000;
26
+ /**
27
+ * Checks if BYOK is currently enabled for the user.
28
+ */
29
+ export async function isByokEnabled() {
30
+ const { loadCredentials } = await import('./credentialStore.js');
31
+ const creds = await loadCredentials();
32
+ return !!(creds.useByok && creds.geminiApiKey);
33
+ }
@@ -4,6 +4,8 @@ export interface StoredCredentials {
4
4
  idToken?: string;
5
5
  refreshToken?: string;
6
6
  idTokenExpiry?: number;
7
+ geminiApiKey?: string;
8
+ useByok?: boolean;
7
9
  }
8
10
  /**
9
11
  * Persists authentication credentials to the most secure available store.
@@ -15,6 +17,11 @@ export interface StoredCredentials {
15
17
  * 4. AES-256-GCM encrypted file with 0600 permissions
16
18
  */
17
19
  export declare function saveCredentials(data: StoredCredentials): Promise<void>;
20
+ /**
21
+ * Updates a specific credential field without reloading all existing fields.
22
+ * Useful for partial updates (e.g., toggling BYOK).
23
+ */
24
+ export declare function updateCredentialField<K extends keyof StoredCredentials>(key: K, value: StoredCredentials[K]): Promise<void>;
18
25
  /**
19
26
  * Loads authentication credentials from the secure store.
20
27
  * Returns an empty object if no credentials are found.
@@ -388,6 +388,14 @@ export async function saveCredentials(data) {
388
388
  }
389
389
  }
390
390
  }
391
+ /**
392
+ * Updates a specific credential field without reloading all existing fields.
393
+ * Useful for partial updates (e.g., toggling BYOK).
394
+ */
395
+ export async function updateCredentialField(key, value) {
396
+ const current = await loadCredentials();
397
+ await saveCredentials({ ...current, [key]: value });
398
+ }
391
399
  /**
392
400
  * Loads authentication credentials from the secure store.
393
401
  * Returns an empty object if no credentials are found.
@@ -1,9 +1,20 @@
1
1
  import * as path from 'node:path';
2
+ const C_STYLE_EXTS = new Set([
3
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts',
4
+ '.css', '.scss', '.sass', '.less', '.vue', '.svelte',
5
+ '.java', '.kt', '.kts', '.swift', '.cs',
6
+ '.cpp', '.c', '.h', '.hpp', '.cc', '.cxx', '.hh', '.hxx',
7
+ '.go', '.rs', '.php', '.dart',
8
+ ]);
9
+ const PYTHON_EXTS = new Set(['.py', '.pyi']);
10
+ const HASH_STYLE_EXTS = new Set(['.yaml', '.yml', '.toml', '.sh', '.bash', '.zsh', '.env']);
2
11
  export function validateSyntax(content, filePath) {
3
12
  const ext = path.extname(filePath).toLowerCase();
13
+ const fileName = path.basename(filePath).toLowerCase();
4
14
  const errors = [];
5
- // Fast check for truncation markers from AI
6
- if (/(\/\/|\/\*)\s*\.\.\./.test(content) || /<!--\s*\.\.\.\s*-->/.test(content)) {
15
+ // Fast check for standalone truncation markers from AI (e.g., lines containing only "// ...")
16
+ if (/^\s*(\/\/|\/\*|#)\s*\.\.\.\s*(\*\/)?\s*$/m.test(content) ||
17
+ /^\s*<!--\s*\.\.\.\s*-->\s*$/m.test(content)) {
7
18
  errors.push('File contains a truncation marker (e.g., "// ..."). Please output the complete file content without truncating.');
8
19
  }
9
20
  // Language specific checks
@@ -14,11 +25,25 @@ export function validateSyntax(content, filePath) {
14
25
  catch (err) {
15
26
  errors.push(`Invalid JSON syntax: ${err instanceof Error ? err.message : String(err)}`);
16
27
  }
28
+ return {
29
+ valid: errors.length === 0,
30
+ errors,
31
+ };
17
32
  }
18
- else if (['.ts', '.tsx', '.js', '.jsx', '.css', '.scss'].includes(ext)) {
19
- const braceBalance = countBalance(content, '{', '}');
20
- const bracketBalance = countBalance(content, '[', ']');
21
- const parenBalance = countBalance(content, '(', ')');
33
+ let family = 'none';
34
+ if (C_STYLE_EXTS.has(ext)) {
35
+ family = 'c-style';
36
+ }
37
+ else if (PYTHON_EXTS.has(ext)) {
38
+ family = 'python';
39
+ }
40
+ else if (HASH_STYLE_EXTS.has(ext) || fileName === 'dockerfile' || fileName === 'makefile') {
41
+ family = 'hash-style';
42
+ }
43
+ if (family !== 'none') {
44
+ const braceBalance = countBalance(content, '{', '}', family);
45
+ const bracketBalance = countBalance(content, '[', ']', family);
46
+ const parenBalance = countBalance(content, '(', ')', family);
22
47
  if (braceBalance > 0)
23
48
  errors.push(`Unmatched opening brace '{' (missing ${braceBalance} closing braces)`);
24
49
  if (braceBalance < 0)
@@ -37,41 +62,120 @@ export function validateSyntax(content, filePath) {
37
62
  errors,
38
63
  };
39
64
  }
40
- function countBalance(text, openChar, closeChar) {
65
+ function countBalance(text, openChar, closeChar, lang) {
41
66
  let balance = 0;
42
- let inString = false;
43
- let stringChar = '';
67
+ let stringStack = [];
68
+ let inRegex = false;
69
+ let inCharClass = false;
70
+ let lastCodeChar = '';
44
71
  for (let i = 0; i < text.length; i++) {
45
72
  const char = text[i];
46
- // Skip string contents
47
- if (inString) {
73
+ const topString = stringStack[stringStack.length - 1];
74
+ if (topString && topString !== '${') {
75
+ if (char === '\\') {
76
+ i++; // Skip escaped character
77
+ continue;
78
+ }
79
+ // Handle Triple Quotes in Python
80
+ if (topString === '"""' || topString === "'''") {
81
+ if (text.slice(i, i + 3) === topString) {
82
+ stringStack.pop();
83
+ i += 2;
84
+ }
85
+ continue;
86
+ }
87
+ if (char === topString) {
88
+ stringStack.pop();
89
+ continue;
90
+ }
91
+ if (topString === '`' && char === '$' && text[i + 1] === '{') {
92
+ stringStack.push('${');
93
+ i++; // Skip {
94
+ continue;
95
+ }
96
+ if (char === '\n' && topString !== '`') {
97
+ // Single-line strings reset on newline
98
+ stringStack.pop();
99
+ }
100
+ continue;
101
+ }
102
+ if (topString === '${') {
103
+ if (char === '}') {
104
+ stringStack.pop();
105
+ continue;
106
+ }
107
+ // Fall through to normal code parsing inside ${...}
108
+ }
109
+ if (inRegex) {
48
110
  if (char === '\\') {
49
111
  i++; // Skip escaped character
50
112
  continue;
51
113
  }
52
- if (char === stringChar) {
53
- inString = false;
114
+ if (char === '\n') {
115
+ inRegex = false;
116
+ inCharClass = false;
117
+ continue;
118
+ }
119
+ if (char === '[' && !inCharClass) {
120
+ inCharClass = true;
121
+ continue;
122
+ }
123
+ if (char === ']' && inCharClass) {
124
+ inCharClass = false;
125
+ continue;
126
+ }
127
+ if (char === '/' && !inCharClass) {
128
+ inRegex = false;
129
+ continue;
54
130
  }
55
131
  continue;
56
132
  }
57
- // Entering a string
58
- if (char === '"' || char === "'" || char === '`') {
59
- inString = true;
60
- stringChar = char;
133
+ // Hash comments (Python, Shell, YAML, etc.)
134
+ if ((lang === 'python' || lang === 'hash-style') && char === '#') {
135
+ const nextLine = text.indexOf('\n', i);
136
+ i = nextLine !== -1 ? nextLine : text.length;
61
137
  continue;
62
138
  }
63
- // Basic comment skipping
64
- if (char === '/' && text[i + 1] === '/') {
139
+ // C-style single-line comment
140
+ if (lang === 'c-style' && char === '/' && text[i + 1] === '/') {
65
141
  const nextLine = text.indexOf('\n', i);
66
142
  i = nextLine !== -1 ? nextLine : text.length;
67
143
  continue;
68
144
  }
69
- // Block comment skip
70
- if (char === '/' && text[i + 1] === '*') {
145
+ // C-style block comment
146
+ if (lang === 'c-style' && char === '/' && text[i + 1] === '*') {
71
147
  const nextEnd = text.indexOf('*/', i + 2);
72
148
  i = nextEnd !== -1 ? nextEnd + 1 : text.length;
73
149
  continue;
74
150
  }
151
+ // HTML block comment
152
+ if (char === '<' && text.slice(i, i + 4) === '<!--') {
153
+ const nextEnd = text.indexOf('-->', i + 4);
154
+ i = nextEnd !== -1 ? nextEnd + 2 : text.length;
155
+ continue;
156
+ }
157
+ // Python triple quotes
158
+ if (lang === 'python' && (text.slice(i, i + 3) === '"""' || text.slice(i, i + 3) === "'''")) {
159
+ stringStack.push(text.slice(i, i + 3));
160
+ i += 2;
161
+ continue;
162
+ }
163
+ // Entering string
164
+ if (char === '"' || char === "'" || (lang === 'c-style' && char === '`')) {
165
+ stringStack.push(char);
166
+ continue;
167
+ }
168
+ // Entering regex (C-style)
169
+ if (lang === 'c-style' && char === '/') {
170
+ if (!lastCodeChar || /[=+(,;:!&|?~^<{[-]+/.test(lastCodeChar)) {
171
+ inRegex = true;
172
+ inCharClass = false;
173
+ continue;
174
+ }
175
+ }
176
+ if (!/\s/.test(char)) {
177
+ lastCodeChar = char;
178
+ }
75
179
  if (char === openChar)
76
180
  balance++;
77
181
  else if (char === closeChar)