minovative-mind-cli 1.3.0 → 1.4.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.
package/README.md CHANGED
@@ -1,43 +1,47 @@
1
1
  # Minovative Mind CLI
2
2
 
3
- ## The CLI agent that makes Flash models perform like Pro.
4
-
5
- ### The result: **Genuine Pro-level accuracy at Flash-level cost.**
3
+ ## The CLI agent that makes Flash models perform like a Pro reasoning model.
6
4
 
7
5
  Most AI coding tools throw your entire codebase at the biggest model available
8
- and hope for the best. This CLI does the opposite — it uses a custom built
6
+ and hope for the best.
7
+
8
+ This CLI does the opposite — it uses a custom built agentic system called,
9
9
  **Precision-Context Verification (PCV)** engine to feed lightweight Flash models
10
- exactly the right context, verify the output compiles, and self-correct, if it even needs to, until
11
- the build is green.
10
+ exactly the right context, execute code, verify the output compiles, and self-correct (if it even needs to), until
11
+ the build/performance metrics are green.
12
+
13
+ > The result: **Genuine Pro reasoning accuracy at Flash-level speed and efficiency.**
12
14
 
13
15
  [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)
14
16
  [![Version](https://img.shields.io/npm/v/minovative-mind-cli.svg)](https://npmjs.org/package/minovative-mind-cli)
15
17
  [![Downloads/week](https://img.shields.io/npm/dw/minovative-mind-cli.svg)](https://npmjs.org/package/minovative-mind-cli)
16
18
 
19
+ - Official Website: [https://www.minovativemind.dev/](https://www.minovativemind.dev/)
20
+ - Latest Updates: [https://www.minovativemind.dev/updates](https://www.minovativemind.dev/updates)
21
+
17
22
  ---
18
23
 
19
24
  ## How it works
20
25
 
21
- Standard AI coding agents: big model + noisy context = expensive and redundant mistakes
22
-
23
- Minovative Mind CLI: flash models + precise context + verified output = better results.
26
+ - Standard AI coding agents: big model + noisy context = expensive and redundant mistakes
27
+ - Minovative Mind CLI: flash models + precise context + verified output = better and cheaper results.
24
28
 
25
- The PCV engine runs in three stages before returning a single line of code:
29
+ ### The PCV engine runs in three stages before returning a single line of code:
26
30
 
27
31
  **1. Investigate**
28
- A dedicated Context Agent reads your project structure, traces import
29
- dependencies, detects recently modified files, and searches the web for
30
- external documentation all before the coding agent touches anything.
32
+
33
+ - **Maps your project:** Traces import dependencies and identifies recently modified files to understand your focus.
34
+ - **Researches context:** Gathers external web documentation and completes background research before touching any code.
31
35
 
32
36
  **2. Execute with precision**
33
- Instead of dumping your entire codebase into a prompt, the engine feeds the
34
- coding agent a compressed, surgical snapshot of only what's relevant.
35
- 40–50% fewer broken generations on large files vs. standard zero-shot prompts.
37
+
38
+ - **Surgical snapshots:** Feeds the agent a compressed, relevant slice of code instead of dumping the entire codebase.
39
+ - **Higher success rates:** Delivers 40–50% fewer broken generations on large files compared to standard zero-shot prompts.
36
40
 
37
41
  **3. Verify and self-correct**
38
- After every change, it runs your actual build (`npm run build`, `cargo check`,
39
- `make`, etc.), reads the exact compiler output, and autonomously fixes errors —
40
- up to 3 correction cycles without you typing a word.
42
+
43
+ - **Runs native builds & audits:** Automatically executes your actual build commands (`npm run build`, `cargo check`, etc.) and scans for performance flaws on modified files only.
44
+ - **Autonomously debugs:** Reads the exact compiler output and fixes its own errors across up to 3 hands-free correction cycles.
41
45
 
42
46
  ---
43
47
 
@@ -54,6 +58,12 @@ up to 3 correction cycles without you typing a word.
54
58
  npm install -g minovative-mind-cli
55
59
  ```
56
60
 
61
+ ### Update to latest version
62
+
63
+ ```bash
64
+ npm update -g minovative-mind-cli
65
+ ```
66
+
57
67
  Create a free account at [minovativemind.dev](https://www.minovativemind.dev),
58
68
  then:
59
69
 
@@ -63,7 +73,7 @@ cd your-project
63
73
  minovative-mind-cli chat # Start coding
64
74
  ```
65
75
 
66
- Works in any terminal — SSH, Docker, CI pipelines, Vim, anywhere Node runs.
76
+ Works in any terminal — SSH, Docker, CI pipelines, Vim, Windows Command Prompt, PowerShell, anywhere Node runs.
67
77
 
68
78
  ---
69
79
 
@@ -77,9 +87,9 @@ Hot-swap during a session with `/models`:
77
87
  | **Gemini 3.1 Pro** | Complex architectural changes |
78
88
  | **Gemini 3.1 Flash Lite** | Maximum speed and cost efficiency |
79
89
 
80
- > Intent routing, context compression, and commit generation always run on
81
- > lightweight models automatically you only pay Pro rates when you
82
- > explicitly ask for it.
90
+ > Background tasks (routing, compression, commits) always use lightweight
91
+ > models automatically. You only pay for what your selected model costs
92
+ > during chat and code execution. Use `/debug` to see exactly what's running.
83
93
 
84
94
  ---
85
95
 
@@ -97,6 +107,34 @@ Hot-swap during a session with `/models`:
97
107
 
98
108
  ---
99
109
 
110
+ ## 🌐 Supported Languages
111
+
112
+ Minovative Mind CLI features deep, context-aware analysis across **11 major programming language families**. Our core engines—**Smart Dependency Tracing**, **Performance Auditing**, and **Ephemeral Analysis Scripts**—provide tailored support depending on the language's syntax and runtime model:
113
+
114
+ | Language Family | Supported Extensions | Dependency Tracing | Performance Auditing | Ephemeral Analysis |
115
+ | :--------------------------- | :------------------------------------------- | :----------------: | :------------------: | :----------------: |
116
+ | **JavaScript / TypeScript** | `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs` | ✅ | ✅ | ✅ |
117
+ | **Python** | `.py`, `.pyw` | ✅ | ✅ | ✅ |
118
+ | **Rust** | `.rs` | ✅ | ✅ | ✅ |
119
+ | **Go** | `.go` | ✅ | ✅ | ✅ |
120
+ | **C / C++** | `.c`, `.h`, `.cpp`, `.hpp`, `.cc` | ✅ | ⚠️ \* | ⚠️ \* |
121
+ | **Java / Kotlin** | `.java`, `.kt`, `.kts` | ✅ | ⚠️ \* | ⚠️ \* |
122
+ | **PHP** | `.php` | ✅ | ✅ | ❌ |
123
+ | **CSS / SCSS / SASS / LESS** | `.css`, `.scss`, `.sass`, `.less` | ✅ | ❌ | ❌ |
124
+ | **Ruby** | `.rb` | ✅ | ❌ | ❌ |
125
+ | **Swift** | `.swift` | ✅ | ❌ | ❌ |
126
+ | **Dart** | `.dart` | ✅ | ❌ | ❌ |
127
+
128
+ _⚠️\*Support is limited or requires custom local system tooling/environment setup._
129
+
130
+ ### Why do some features only support specific languages?
131
+
132
+ 1. **Dependency Tracing:** Fully supported for all 11 languages. It uses lightning-fast static regex pattern matching to resolve local import structures and determine the "blast radius" of code changes without requiring local compilations.
133
+ 2. **Performance Auditing:** Uses regex-based heuristic analysis to scan for issues. Extending this to complex grammars (such as Ruby or Swift) with pure regex can cause high false-positive rates; those require full AST parsing, which runs counter to the auditor's ultra-fast (<50ms) and zero-dependency goals.
134
+ 3. **Ephemeral Analysis Scripts:** Runs temporary files in sandbox directories to parse and extract local symbols. This requires having local runtimes/interpreters installed (e.g., Node, Python, Rust, Go compilers) and standard single-file execution support, which is not suitable for complex build setups (Java Maven/Gradle, C++ Makefiles) or non-executable languages (CSS).
135
+
136
+ ---
137
+
100
138
  ## Legal
101
139
 
102
140
  [Terms of Service](https://www.minovativemind.dev/legal/cli-terms) ·
@@ -6,6 +6,7 @@ import * as p from '@clack/prompts';
6
6
  import pc from 'picocolors';
7
7
  import { startAgentLoop } from '../services/agent.js';
8
8
  import { getAuthorizedIdToken, login } from '../services/auth.js';
9
+ import { printLogo } from '../utils/logo.js';
9
10
  import { updateWorkspaceStatus } from '../services/workspace.js';
10
11
  /**
11
12
  * @class DefaultCommand
@@ -36,10 +37,7 @@ Chat Controls:
36
37
  /**
37
38
  * Example usages for the chat command shown in help output.
38
39
  */
39
- static examples = [
40
- '<%= config.bin %> chat',
41
- '<%= config.bin %> chat --help',
42
- ];
40
+ static examples = ['<%= config.bin %> chat', '<%= config.bin %> chat --help'];
43
41
  /**
44
42
  * The main execution logic for the chat command.
45
43
  * - Initializes update notifications.
@@ -53,7 +51,8 @@ Chat Controls:
53
51
  // Setup update notifier
54
52
  const pkg = JSON.parse(await fs.promises.readFile(new URL('../../package.json', import.meta.url), 'utf8'));
55
53
  updateNotifier({ pkg }).notify();
56
- p.intro(`${pc.bgCyan(pc.black(' Minovative Mind '))} ${pc.dim('v' + this.config.version)}`);
54
+ printLogo();
55
+ p.intro(`${pc.bgCyan(pc.black(' Minovative Mind CLI '))} ${pc.dim('v' + this.config.version)}`);
57
56
  // Check authentication
58
57
  let idToken = await getAuthorizedIdToken();
59
58
  if (!idToken) {
@@ -1,5 +1,6 @@
1
1
  import { exec } from 'node:child_process';
2
2
  import { promises as fs } from 'node:fs';
3
+ import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import { promisify } from 'node:util';
5
6
  import { SchemaType } from '@google/generative-ai';
@@ -283,7 +284,7 @@ async function getIgnoredPaths(workspaceRoot) {
283
284
  .map((l) => l.trim())
284
285
  .filter((l) => l && !l.startsWith('#'));
285
286
  for (const line of lines) {
286
- const cleanLine = line.replace(/^\//, '').replace(/\/$/, '');
287
+ const cleanLine = line.replace(/\\/g, '/').replace(/^\//, '').replace(/\/$/, '');
287
288
  if (!cleanLine.includes('*')) {
288
289
  ignoredDirs.add(cleanLine);
289
290
  ignoredFiles.add(cleanLine);
@@ -525,18 +526,18 @@ export async function runCommand(workspaceRoot, command, abortSignal) {
525
526
  });
526
527
  let output = [stdout, stderr].filter(Boolean).join('\n');
527
528
  // Truncate command output to prevent memory blowout from massive build logs
528
- const MAX_CMD_OUTPUT = 50_000;
529
+ const MAX_CMD_OUTPUT = 15_000;
529
530
  if (output.length > MAX_CMD_OUTPUT) {
530
- output = output.substring(0, MAX_CMD_OUTPUT) + '\n... (output truncated exceeded 50KB limit)';
531
+ output = output.substring(0, MAX_CMD_OUTPUT) + `\n\n... (Output truncated: ${output.length} bytes exceeded 15KB limit. To view the rest, pipe the command to a file and read it in chunks, or use grep.)`;
531
532
  }
532
533
  return { output: output || '(command produced no output)' };
533
534
  }
534
535
  catch (err) {
535
536
  const message = err instanceof Error ? err.message : String(err);
536
537
  // Also truncate error output
537
- const MAX_ERR_OUTPUT = 30_000;
538
+ const MAX_ERR_OUTPUT = 15_000;
538
539
  const truncatedMsg = message.length > MAX_ERR_OUTPUT
539
- ? message.substring(0, MAX_ERR_OUTPUT) + '\n... (error output truncated)'
540
+ ? message.substring(0, MAX_ERR_OUTPUT) + `\n\n... (Error output truncated: ${message.length} bytes exceeded 15KB limit. Pipe to a file if you need full logs.)`
540
541
  : message;
541
542
  return { output: '', error: `Command failed: ${truncatedMsg}` };
542
543
  }
@@ -544,29 +545,86 @@ export async function runCommand(workspaceRoot, command, abortSignal) {
544
545
  export async function grepSearch(workspaceRoot, pattern, fileGlob, abortSignal) {
545
546
  try {
546
547
  const { ignoredDirs } = await getIgnoredPaths(workspaceRoot);
547
- const excludeDirArgs = Array.from(ignoredDirs)
548
- .map((dir) => `--exclude-dir="${dir}"`)
549
- .concat('--exclude-dir=".*"') // Exclude all hidden directories
550
- .join(' ');
551
- const excludeFileArgs = EXCLUDED_EXTENSIONS.map((ext) => `--exclude="${ext}"`)
552
- .concat('--exclude=".*"') // Exclude all hidden files
553
- .join(' ');
554
- // Build a grep command that works cross-platform via node
555
- // We use grep -rnIEi for recursive, line numbers, skip binary, extended regex, case-insensitive
556
- const globArg = fileGlob ? ` --include="${fileGlob}"` : '';
557
- const cmd = `grep -rnIEi --color=never ${excludeDirArgs} ${excludeFileArgs}${globArg} "${pattern.replace(/"/g, '\\"')}" .`;
558
- const { stdout } = await execAsync(cmd, {
559
- cwd: workspaceRoot,
560
- timeout: 15_000,
561
- maxBuffer: 1024 * 1024 * 2,
562
- signal: abortSignal,
563
- });
564
- // Limit output to 50 results
565
- const lines = stdout.trim().split('\n');
566
- const limited = lines.slice(0, 50);
567
- const resultText = limited.join('\n') + (lines.length > 50 ? `\n\n... (${lines.length - 50} more results truncated)` : '');
568
- if (!resultText)
548
+ if (os.platform() !== 'win32') {
549
+ const excludeDirArgs = Array.from(ignoredDirs)
550
+ .map((dir) => `--exclude-dir="${dir}"`)
551
+ .concat('--exclude-dir=".*"') // Exclude all hidden directories
552
+ .join(' ');
553
+ const excludeFileArgs = EXCLUDED_EXTENSIONS.map((ext) => `--exclude="${ext}"`)
554
+ .concat('--exclude=".*"') // Exclude all hidden files
555
+ .join(' ');
556
+ // Build a grep command that works cross-platform via node
557
+ // We use grep -rnIEi for recursive, line numbers, skip binary, extended regex, case-insensitive
558
+ const globArg = fileGlob ? ` --include="${fileGlob}"` : '';
559
+ const cmd = `grep -rnIEi --color=never ${excludeDirArgs} ${excludeFileArgs}${globArg} "${pattern.replace(/"/g, '\\"')}" .`;
560
+ const { stdout } = await execAsync(cmd, {
561
+ cwd: workspaceRoot,
562
+ timeout: 15_000,
563
+ maxBuffer: 1024 * 1024 * 2,
564
+ signal: abortSignal,
565
+ });
566
+ // Limit output to 50 results
567
+ const lines = stdout.trim().split('\n');
568
+ const limited = lines.slice(0, 50);
569
+ const resultText = limited.join('\n') + (lines.length > 50 ? `\n\n... (${lines.length - 50} more results truncated)` : '');
570
+ if (!resultText)
571
+ return { output: `No matches found for "${pattern}".` };
572
+ const wrappedResult = `<workspace_file path="grep_search_results">\n<content_data><![CDATA[\n${sanitizeForCDATA(resultText)}\n]]></content_data>\n</workspace_file>`;
573
+ return { output: wrappedResult };
574
+ }
575
+ // Node.js fallback for Windows
576
+ const regexPattern = new RegExp(pattern, 'gi'); // Emulate grep -i and global match
577
+ const globPattern = fileGlob ? new RegExp('^' + fileGlob.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$', 'i') : null;
578
+ const results = [];
579
+ async function walk(dir) {
580
+ if (results.length >= 50)
581
+ return;
582
+ let entries;
583
+ try {
584
+ entries = await fs.readdir(dir, { withFileTypes: true });
585
+ }
586
+ catch {
587
+ return;
588
+ }
589
+ for (const entry of entries) {
590
+ if (results.length >= 50)
591
+ return;
592
+ if (entry.name.startsWith('.'))
593
+ continue;
594
+ if (entry.isDirectory()) {
595
+ if (ignoredDirs.has(entry.name))
596
+ continue;
597
+ await walk(path.join(dir, entry.name));
598
+ }
599
+ else if (entry.isFile()) {
600
+ if (EXCLUDED_EXTENSIONS.some((ext) => entry.name.endsWith(ext.replace('*', ''))))
601
+ continue;
602
+ if (globPattern && !globPattern.test(entry.name))
603
+ continue;
604
+ try {
605
+ const filePath = path.join(dir, entry.name);
606
+ const content = await fs.readFile(filePath, 'utf-8');
607
+ const lines = content.split(/\r?\n/);
608
+ for (let i = 0; i < lines.length; i++) {
609
+ if (results.length >= 50)
610
+ break;
611
+ regexPattern.lastIndex = 0;
612
+ if (regexPattern.test(lines[i])) {
613
+ const relPath = path.relative(workspaceRoot, filePath).replace(/\\/g, '/');
614
+ results.push(`${relPath}:${i + 1}:${lines[i]}`);
615
+ }
616
+ }
617
+ }
618
+ catch {
619
+ // Ignore unreadable files (binaries, etc)
620
+ }
621
+ }
622
+ }
623
+ }
624
+ await walk(workspaceRoot);
625
+ if (results.length === 0)
569
626
  return { output: `No matches found for "${pattern}".` };
627
+ const resultText = results.join('\n') + (results.length >= 50 ? `\n\n... (more results truncated)` : '');
570
628
  const wrappedResult = `<workspace_file path="grep_search_results">\n<content_data><![CDATA[\n${sanitizeForCDATA(resultText)}\n]]></content_data>\n</workspace_file>`;
571
629
  return { output: wrappedResult };
572
630
  }
@@ -633,7 +691,7 @@ export async function findRecentChanges(workspaceRoot, dirPath = '.', minutes =
633
691
  const filePath = path.join(currentPath, entry.name);
634
692
  const stats = await fs.stat(filePath);
635
693
  if (stats.mtimeMs >= thresholdMs) {
636
- const relPath = path.relative(workspaceRoot, filePath);
694
+ const relPath = path.relative(workspaceRoot, filePath).replace(/\\/g, '/');
637
695
  recentFiles.push({ path: relPath, mtime: stats.mtimeMs });
638
696
  }
639
697
  }
@@ -698,9 +756,13 @@ export async function runDebugScript(workspaceRoot, language, code, abortSignal)
698
756
  case 'go':
699
757
  cmd = `go run ${tmpFileName}`;
700
758
  break;
701
- case 'rust':
702
- cmd = `rustc ${tmpFileName} && ./${tmpFileName.replace('.rs', '')}`;
759
+ case 'rust': {
760
+ const binExt = os.platform() === 'win32' ? '.exe' : '';
761
+ const binPath = tmpFileName.replace('.rs', binExt);
762
+ const runCmd = os.platform() === 'win32' ? binPath : `./${binPath}`;
763
+ cmd = `rustc ${tmpFileName} -o ${binPath} && ${runCmd}`;
703
764
  break;
765
+ }
704
766
  default:
705
767
  return { output: '', error: `Unsupported language runtime: ${language}` };
706
768
  }
@@ -738,7 +800,8 @@ export async function runDebugScript(workspaceRoot, language, code, abortSignal)
738
800
  try {
739
801
  await fs.rm(absPath, { force: true });
740
802
  if (language.toLowerCase() === 'rust') {
741
- const binPath = absPath.replace('.rs', '');
803
+ const binExt = os.platform() === 'win32' ? '.exe' : '';
804
+ const binPath = absPath.replace('.rs', binExt);
742
805
  await fs.rm(binPath, { force: true });
743
806
  }
744
807
  }
@@ -22,6 +22,8 @@ import path from 'node:path';
22
22
  import { exec } from 'node:child_process';
23
23
  import { promisify } from 'node:util';
24
24
  import { debugLog, toggleDebugMode } from '../utils/logger.js';
25
+ import { ensureProjectStorage, ensureIgnored, readCache, writeCache, invalidateCacheForDependents } from '../utils/projectStorage.js';
26
+ import * as crypto from 'node:crypto';
25
27
  const execAsync = promisify(exec);
26
28
  /**
27
29
  * Asynchronous Input Handler (AsyncInputHandler)
@@ -181,6 +183,7 @@ export class AsyncInputHandler {
181
183
  const originalMessage = spinner.message.bind(spinner);
182
184
  spinner.message = (msg) => {
183
185
  if (msg) {
186
+ ;
184
187
  spinner._lastMessage = msg;
185
188
  }
186
189
  originalMessage(msg);
@@ -188,6 +191,7 @@ export class AsyncInputHandler {
188
191
  const originalStart = spinner.start.bind(spinner);
189
192
  spinner.start = (msg) => {
190
193
  if (msg) {
194
+ ;
191
195
  spinner._lastMessage = msg;
192
196
  }
193
197
  originalStart(msg);
@@ -231,6 +235,7 @@ import { consumeSkipOnce, executeTool, getApprovalMode, setApprovalMode } from '
231
235
  import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, ProxyChatSession, compressTextUsingFlashLite, } from './ai.js';
232
236
  import { changeLogger } from './changeLogger.js';
233
237
  import { gatherContext, routeIntent } from './contextAgent.js';
238
+ import { printLogo } from '../utils/logo.js';
234
239
  import { verifyChangedFiles } from './verificationService.js';
235
240
  import { buildContextInjection } from '../utils/contextPrompts.js';
236
241
  import { readPaste } from '../utils/paste.js';
@@ -309,30 +314,108 @@ function formatToolCall(name, args) {
309
314
  const details = formatter ? formatter() : '';
310
315
  return `${icon} ${label}${details}`;
311
316
  }
317
+ function isCommandDestructive(command) {
318
+ const destructivePatterns = [
319
+ // Unix deletion (catches rm -r, rm -rf, rm -fr, rm --recursive, rm --force, anywhere in args)
320
+ /\brm\s+.*?(?:-[a-zA-Z]*[rfR]|--(?:recursive|force))\b/i,
321
+ /\bmv\s+.*\/dev\/null\b/i, // Move to void
322
+ // Windows deletion (catches rd /s, del /f /s)
323
+ /\b(?:rd|rmdir)\s+.*\/[sq]\b/i,
324
+ /\bdel\s+.*\/[fs]\b/i,
325
+ // Remote script execution (catches curl/wget piped to any shell or runtime)
326
+ /\b(?:curl|wget)\b.*?\|\s*(?:sh|bash|zsh|dash|python|node|ruby|perl)\b/i,
327
+ // Git overwriting (catches hard resets, clean -f, force pushes, hard branch deletes)
328
+ /\bgit\s+(?:reset|clean)\s+.*(?:--hard|-f|--force)\b/i,
329
+ /\bgit\s+push\s+.*(?:-f|--force)\b/i,
330
+ /\bgit\s+branch\s+.*-D\b/i,
331
+ // DB & Cluster Drops (catches truncates, deletes, drop db/table, docker volume/system prunes, kubectl wipes)
332
+ /\b(?:drop|truncate|delete)\s+(?:database|table|schema)\b/i,
333
+ /\bdocker\s+system\s+prune\b/i,
334
+ /\bdocker\s+volume\s+(?:rm|prune)\b/i,
335
+ /\bkubectl\s+delete\s+(?:namespace|all|--all)\b/i,
336
+ // Disk & System destruction (format, fdisk, dd, overwriting block devices, shutdown, fork bombs)
337
+ /\b(?:mkfs|fdisk|parted|mkswap)\b/i,
338
+ /\bformat\s+[A-Z]:/i,
339
+ /\bdd\s+.*(?:if=|of=)\b/i,
340
+ />\s*\/dev\/(?:sda|disk|hda|nvme)\b/i,
341
+ /\b(?:shutdown|reboot|halt|poweroff)\b/i,
342
+ /:\(\)\{\s*:\|:&\s*\};:/, // Fork bomb
343
+ // User data/config erasure
344
+ /\bcrontab\s+-r\b/i,
345
+ /\bhistory\s+-c\b/i,
346
+ // Massive permission shifts (chmod 777, recursive chowns)
347
+ /\bchmod\s+(?:-[R\w]*\s+)?777\b/i,
348
+ /\bchown\s+-[R\w]*\b/i,
349
+ ];
350
+ return destructivePatterns.some((pattern) => pattern.test(command));
351
+ }
352
+ function isCommandSafe(command) {
353
+ // Do not auto-approve chained, redirected, or sudo commands
354
+ if (/[;&|>]/.test(command) || /\bsudo\b/.test(command)) {
355
+ return false;
356
+ }
357
+ const safePatterns = [
358
+ /^\s*ls\b/i,
359
+ /^\s*pwd\b/i,
360
+ /^\s*whoami\b/i,
361
+ /^\s*cat\b/i,
362
+ /^\s*echo\b/i,
363
+ /^\s*grep\b/i,
364
+ /^\s*find\b/i,
365
+ /^\s*npm\s+(install|i|ci|run\b)/i,
366
+ /^\s*yarn\s+(install|add|build|lint|test|run\b)/i,
367
+ /^\s*pnpm\s+(install|i|add|build|lint|test|run\b)/i,
368
+ /^\s*bun\s+(install|i|add|run\b)/i,
369
+ /^\s*cargo\s+(build|check|add|test|run\b)/i,
370
+ /^\s*go\s+(mod|get|build|test|run\b)/i,
371
+ /^\s*pip\s+(install|list|show)\b/i,
372
+ /^\s*uv\s+(add|pip|sync|run\b)/i,
373
+ /^\s*rustc\b/i,
374
+ /^\s*git\s+(status|log|diff|show|branch)\b/i,
375
+ /^\s*(node|python|ruby|java|go|rustc)\s+(--version|-v)\b/i,
376
+ /^\s*tsc\b/i,
377
+ /^\s*eslint\b/i,
378
+ /^\s*prettier\b/i,
379
+ ];
380
+ return safePatterns.some((pattern) => pattern.test(command));
381
+ }
312
382
  /**
313
- * Prompts the user to approve a pending shell command requested by the AI.
314
- * Implements a safety gate to protect the host machine's environment from arbitrary code execution.
383
+ * Executes an interactive approval prompt via `@clack/prompts`.
315
384
  *
316
- * Supports three approval models:
317
- * 1. **Ask**: Prompts the user for every shell execution command.
385
+ * Supports pattern-based approvals and three user approval models:
386
+ * 1. **Ask**: Prompts the user for every shell execution command (except implicitly safe ones).
318
387
  * 2. **Skip-Once**: Automatically grants permission to the current command, then resets.
319
- * 3. **Skip-All / Auto-Approve**: Grants permission to all future terminal commands in the current session.
388
+ * 3. **Skip-All / Auto-Approve**: Grants permission to all future terminal commands.
389
+ *
390
+ * Destructive commands will ALWAYS prompt the user, regardless of mode.
320
391
  *
321
392
  * @param command - The terminal string requested for execution.
322
393
  * @returns A promise resolving to `true` if approved, or `false` if denied/cancelled.
323
394
  */
324
395
  async function requestCommandApproval(command) {
325
396
  const mode = getApprovalMode();
326
- if (mode === 'skip-all') {
327
- p.log.info(`${pc.dim('Auto-approved (skip-all):')} ${pc.yellow(command)}`);
328
- return true;
397
+ const isDestructive = isCommandDestructive(command);
398
+ const isSafe = isCommandSafe(command);
399
+ // Force prompt for destructive commands
400
+ if (isDestructive) {
401
+ p.log.warn(`${pc.bgRed(pc.white(' WARNING '))} Destructive command detected. Explicit approval required.`);
329
402
  }
330
- if (mode === 'skip-once') {
331
- p.log.info(`${pc.dim('Auto-approved (skip-once):')} ${pc.yellow(command)}`);
332
- consumeSkipOnce();
333
- return true;
403
+ else {
404
+ if (mode === 'skip-all') {
405
+ p.log.info(`${pc.dim('Auto-approved (skip-all):')} ${pc.yellow(command)}`);
406
+ return true;
407
+ }
408
+ if (mode === 'skip-once') {
409
+ p.log.info(`${pc.dim('Auto-approved (skip-once):')} ${pc.yellow(command)}`);
410
+ consumeSkipOnce();
411
+ return true;
412
+ }
413
+ if (isSafe) {
414
+ p.log.info(`${pc.dim('Auto-approved (safe command):')} ${pc.yellow(command)}`);
415
+ return true;
416
+ }
334
417
  }
335
- // mode === 'ask'
418
+ // mode === 'ask' or isDestructive
336
419
  const result = await p.select({
337
420
  message: `Approve command: ${pc.yellow(command)}`,
338
421
  options: [
@@ -599,6 +682,11 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
599
682
  * @param version - The Semantic Version string of the active tool distribution.
600
683
  */
601
684
  export async function startAgentLoop(workspaceRoot, version) {
685
+ // Initialize project-level storage and ensure it is safely ignored from version control
686
+ ensureProjectStorage(workspaceRoot);
687
+ ensureIgnored(workspaceRoot);
688
+ // Initialize persistent change ledger
689
+ changeLogger.init(workspaceRoot);
602
690
  const chat = createSharedChatSession();
603
691
  const inputHandler = new AsyncInputHandler();
604
692
  let isRawPasteMode = false;
@@ -643,8 +731,9 @@ export async function startAgentLoop(workspaceRoot, version) {
643
731
  }
644
732
  }
645
733
  let line = (userInputRaw || '');
646
- if (line.trimEnd().endsWith('\\')) {
647
- const cleanLine = line.trimEnd().slice(0, -1);
734
+ // Require a space before the backslash to prevent Windows directory paths (e.g., C:\foo\) from triggering multiline
735
+ if (line.trimEnd().endsWith(' \\')) {
736
+ const cleanLine = line.trimEnd().slice(0, -2);
648
737
  lines.push(cleanLine);
649
738
  isMultiLine = true;
650
739
  // Clack's text prompt dims the submitted value, which can be completely invisible on some terminal themes.
@@ -720,7 +809,8 @@ export async function startAgentLoop(workspaceRoot, version) {
720
809
  else if (userInput.toLowerCase() === '/clear') {
721
810
  chat.clearHistory();
722
811
  process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); // Hard clear screen and scrollback
723
- p.intro(`${pc.bgCyan(pc.black(' Minovative Mind '))} ${pc.dim('v' + version)}`);
812
+ printLogo();
813
+ p.intro(`${pc.bgCyan(pc.black(' Minovative Mind CLI '))} ${pc.dim('v' + version)}`);
724
814
  p.log.info(`${pc.dim('Workspace:')} ${pc.cyan(workspaceRoot)}`);
725
815
  p.log.info(`${pc.dim('Commands:')} Type ${pc.yellow('/')} to open the command menu and "${pc.yellow('stop')}" to stop the ai generation. Type ${pc.yellow('exit')} to leave.`);
726
816
  p.log.success('Chat history cleared.');
@@ -772,25 +862,62 @@ export async function startAgentLoop(workspaceRoot, version) {
772
862
  continue;
773
863
  }
774
864
  if (userInput.toLowerCase() === '/revert') {
775
- const lastChangeSet = changeLogger.popLastChangeSet();
776
- if (!lastChangeSet) {
865
+ const history = changeLogger.getHistory();
866
+ if (!history || history.length === 0) {
777
867
  p.log.warn('No changes to revert.');
778
868
  continue;
779
869
  }
870
+ const lastChangeSet = history[history.length - 1];
871
+ const truncate = (str, max) => (str.length > max ? str.substring(0, max - 3) + '...' : str);
872
+ const revertMenu = await p.select({
873
+ message: 'Revert Menu',
874
+ options: [
875
+ { value: 'revert_last', label: `Revert last change (${truncate(lastChangeSet.description, 50)})` },
876
+ { value: 'view_history', label: 'View history' },
877
+ { value: 'cancel', label: 'Cancel' },
878
+ ],
879
+ });
880
+ if (p.isCancel(revertMenu) || revertMenu === 'cancel') {
881
+ continue;
882
+ }
883
+ let targetTimestamp = lastChangeSet.timestamp;
884
+ if (revertMenu === 'view_history') {
885
+ // Reverse history so newest is at the top
886
+ const historyOptions = history
887
+ .slice()
888
+ .reverse()
889
+ .map((cs, i) => ({
890
+ value: cs.timestamp,
891
+ label: `[${i === 0 ? 'Latest' : `-${i}`}] ${truncate(cs.description, 50)} (${new Date(cs.timestamp).toLocaleTimeString()})`,
892
+ hint: `Reverts this and all ${i} changes after it`,
893
+ }));
894
+ const selectedHistory = await p.select({
895
+ message: 'Select the point in history to revert back to:',
896
+ options: [...historyOptions, { value: -1, label: 'Cancel' }],
897
+ });
898
+ if (p.isCancel(selectedHistory) || selectedHistory === -1) {
899
+ continue;
900
+ }
901
+ targetTimestamp = selectedHistory;
902
+ }
780
903
  const spinner = p.spinner();
781
- spinner.start('Reverting last changes...');
904
+ spinner.start('Reverting selected changes...');
782
905
  try {
783
- for (const change of lastChangeSet.changes) {
784
- const absPath = path.resolve(workspaceRoot, change.filePath);
785
- if (change.action === 'create') {
786
- await fs.rm(absPath, { force: true });
787
- }
788
- else if (change.originalContent !== null) {
789
- await fs.writeFile(absPath, change.originalContent, 'utf-8');
906
+ const changesToRevert = changeLogger.popUntil(targetTimestamp);
907
+ // changesToRevert is ordered from newest to oldest. We must apply them sequentially.
908
+ for (const changeSet of changesToRevert) {
909
+ for (const change of changeSet.changes) {
910
+ const absPath = path.resolve(workspaceRoot, change.filePath);
911
+ if (change.action === 'create') {
912
+ await fs.rm(absPath, { force: true });
913
+ }
914
+ else if (change.originalContent !== null) {
915
+ await fs.writeFile(absPath, change.originalContent, 'utf-8');
916
+ }
790
917
  }
791
918
  }
792
919
  spinner.stop('Reverted successfully.');
793
- p.log.success(`Reverted changes for: ${lastChangeSet.description}`);
920
+ p.log.success(`Reverted ${changesToRevert.length} changeset(s) successfully.`);
794
921
  }
795
922
  catch (err) {
796
923
  spinner.stop('Revert failed.');
@@ -839,6 +966,7 @@ ${diffOut}
839
966
  continue;
840
967
  }
841
968
  // ─── Main Execution Cycle ────────────────────────────────────────
969
+ const turnStartTime = Date.now();
842
970
  const spinner = p.spinner();
843
971
  const ac = new AbortController();
844
972
  inputHandler.setAbortController(ac);
@@ -890,11 +1018,46 @@ ${diffOut}
890
1018
  let dynamicSystemInstruction = config.systemInstruction;
891
1019
  // Inject gathered workspace directories, dependency configurations, and matching search patterns
892
1020
  if (gatherRes.contextResult) {
893
- let contextInjection = buildContextInjection(gatherRes.contextResult);
894
- debugLog(`Raw context injection size: ${contextInjection.length} chars`);
895
- // Compress context inputs using Gemini Flash Lite to optimize prompt density and prevent token bloat
896
- contextInjection = await compressTextUsingFlashLite(contextInjection, `Summarize the following project context concisely. Preserve all file paths, project types, and the high-level purpose of the read files. Keep it under 2000 characters if possible.\nCRITICAL: Focus specifically on details relevant to the user's current request: "${userInput}"`);
897
- debugLog(`Compressed context injection size: ${contextInjection.length} chars`);
1021
+ const cachedContext = readCache(workspaceRoot, 'context_cache.json') || {};
1022
+ let cacheUpdated = false;
1023
+ // Compress each relevant file individually to allow granular cache hits
1024
+ const compressedFiles = new Map();
1025
+ for (const [filePath, content] of gatherRes.contextResult.relevantFiles.entries()) {
1026
+ if (content.length < 2000) {
1027
+ debugLog(`Bypassing cache and compression for ${filePath} (length ${content.length} < 2000)`);
1028
+ compressedFiles.set(filePath, content);
1029
+ continue;
1030
+ }
1031
+ const fileHash = crypto.createHash('sha256').update(filePath + content).digest('hex');
1032
+ if (cachedContext[fileHash]) {
1033
+ debugLog(`Context cache HIT for file ${filePath} (hash ${fileHash.substring(0, 8)})`);
1034
+ compressedFiles.set(filePath, cachedContext[fileHash]);
1035
+ }
1036
+ else {
1037
+ debugLog(`Context cache MISS for file ${filePath} (hash ${fileHash.substring(0, 8)})`);
1038
+ const compressPrompt = `Summarize the following file contents concisely. Preserve all exports, functions, classes, variables, and architectural purpose. Keep it under 2000 characters if possible. File: ${filePath}`;
1039
+ const summary = await compressTextUsingFlashLite(content, compressPrompt);
1040
+ compressedFiles.set(filePath, summary);
1041
+ cachedContext[fileHash] = summary;
1042
+ cacheUpdated = true;
1043
+ }
1044
+ }
1045
+ if (cacheUpdated) {
1046
+ // Keep cache size manageable by restricting to recent 100 file entries
1047
+ const keys = Object.keys(cachedContext);
1048
+ if (keys.length > 100) {
1049
+ const toDelete = keys.length - 100;
1050
+ for (let i = 0; i < toDelete; i++) {
1051
+ delete cachedContext[keys[i]];
1052
+ }
1053
+ }
1054
+ writeCache(workspaceRoot, 'context_cache.json', cachedContext);
1055
+ }
1056
+ // Replace raw file contents with their compressed summaries
1057
+ gatherRes.contextResult.relevantFiles = compressedFiles;
1058
+ // Assemble the final context injection string
1059
+ const contextInjection = buildContextInjection(gatherRes.contextResult);
1060
+ debugLog(`Final compressed context injection size: ${contextInjection.length} chars`);
898
1061
  dynamicSystemInstruction += '\n\n' + contextInjection;
899
1062
  }
900
1063
  // Apply the dynamic prompt updates and tool registrations to the active chat session
@@ -993,8 +1156,13 @@ ${diffOut}
993
1156
  result = await chat.sendMessage(correctionPrompt);
994
1157
  spinner.stop('');
995
1158
  }
996
- // Persist the verified changes into our session change ledger
997
- changeLogger.commitChangeSet();
1159
+ const postExecutionChanges = changeLogger.getCurrentChangeSet()?.changes || [];
1160
+ const modifiedFiles = postExecutionChanges
1161
+ .filter((c) => c.action === 'modify' || c.action === 'create' || c.action === 'delete')
1162
+ .map((c) => path.relative(workspaceRoot, c.filePath));
1163
+ if (modifiedFiles.length > 0) {
1164
+ await invalidateCacheForDependents(workspaceRoot, modifiedFiles);
1165
+ }
998
1166
  if (finalText) {
999
1167
  console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(${chat.getModel()})`)}\n`);
1000
1168
  // Strip out excessive empty lines generated by LLMs to prevent huge visual gaps in marked-terminal
@@ -1004,11 +1172,20 @@ ${diffOut}
1004
1172
  if (latestUsage && latestUsage.remainingBalance !== undefined) {
1005
1173
  p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(latestUsage.remainingBalance.toLocaleString())}`);
1006
1174
  }
1175
+ const turnEndTime = Date.now();
1176
+ const turnDuration = ((turnEndTime - turnStartTime) / 1000).toFixed(1);
1177
+ p.log.info(`${pc.dim('Generated in')} ${pc.cyan(turnDuration + 's')}`);
1007
1178
  }
1008
1179
  catch (err) {
1009
1180
  spinner.stop('');
1010
1181
  const message = err instanceof Error ? err.message : String(err);
1011
1182
  p.log.error(`${pc.red('Error:')} ${message}`);
1012
1183
  }
1184
+ finally {
1185
+ // Persist any verified or partial changes into our session change ledger.
1186
+ // This ensures that if the process crashes, errors out, or is aborted mid-generation,
1187
+ // the user can still use /revert to undo the partial file mutations.
1188
+ changeLogger.commitChangeSet();
1189
+ }
1013
1190
  }
1014
1191
  }
@@ -11,6 +11,12 @@ export interface ChangeSet {
11
11
  declare class ChangeLogger {
12
12
  private changeStack;
13
13
  private currentChangeSet;
14
+ private workspaceRoot;
15
+ private readonly MAX_HISTORY;
16
+ init(workspaceRoot: string): void;
17
+ private saveState;
18
+ getHistory(): ChangeSet[];
19
+ popUntil(timestamp: number): ChangeSet[];
14
20
  startChangeSet(description: string): void;
15
21
  logChange(filePath: string, originalContent: string | null, action: 'create' | 'modify' | 'delete'): void;
16
22
  commitChangeSet(): void;
@@ -1,6 +1,41 @@
1
+ import { readCache, writeCache } from '../utils/projectStorage.js';
1
2
  class ChangeLogger {
2
3
  changeStack = [];
3
4
  currentChangeSet = null;
5
+ workspaceRoot = '';
6
+ MAX_HISTORY = 10;
7
+ init(workspaceRoot) {
8
+ this.workspaceRoot = workspaceRoot;
9
+ const savedState = readCache(workspaceRoot, 'revert_state.json');
10
+ if (savedState && Array.isArray(savedState)) {
11
+ this.changeStack = savedState;
12
+ }
13
+ }
14
+ saveState() {
15
+ if (!this.workspaceRoot)
16
+ return;
17
+ // Enforce history limit
18
+ if (this.changeStack.length > this.MAX_HISTORY) {
19
+ this.changeStack = this.changeStack.slice(-this.MAX_HISTORY);
20
+ }
21
+ writeCache(this.workspaceRoot, 'revert_state.json', this.changeStack);
22
+ }
23
+ getHistory() {
24
+ return [...this.changeStack];
25
+ }
26
+ popUntil(timestamp) {
27
+ const changesToRevert = [];
28
+ while (this.changeStack.length > 0) {
29
+ const top = this.changeStack[this.changeStack.length - 1];
30
+ changesToRevert.push(top);
31
+ this.changeStack.pop();
32
+ if (top.timestamp === timestamp) {
33
+ break;
34
+ }
35
+ }
36
+ this.saveState();
37
+ return changesToRevert;
38
+ }
4
39
  startChangeSet(description) {
5
40
  if (this.currentChangeSet) {
6
41
  this.commitChangeSet();
@@ -34,6 +69,7 @@ class ChangeLogger {
34
69
  commitChangeSet() {
35
70
  if (this.currentChangeSet && this.currentChangeSet.changes.length > 0) {
36
71
  this.changeStack.push(this.currentChangeSet);
72
+ this.saveState();
37
73
  }
38
74
  this.currentChangeSet = null;
39
75
  }
@@ -48,7 +84,11 @@ class ChangeLogger {
48
84
  popLastChangeSet() {
49
85
  if (this.changeStack.length === 0)
50
86
  return null;
51
- return this.changeStack.pop() || null;
87
+ const popped = this.changeStack.pop() || null;
88
+ if (popped) {
89
+ this.saveState();
90
+ }
91
+ return popped;
52
92
  }
53
93
  hasChanges() {
54
94
  return this.changeStack.length > 0;
@@ -39,7 +39,8 @@ function buildCommand(language, scriptPath) {
39
39
  case 'go':
40
40
  return `go run "${scriptPath}"`;
41
41
  case 'rust': {
42
- const binPath = scriptPath.replace('.rs', '');
42
+ const binExt = os.platform() === 'win32' ? '.exe' : '';
43
+ const binPath = scriptPath.replace('.rs', binExt);
43
44
  return `rustc "${scriptPath}" -o "${binPath}" && "${binPath}"`;
44
45
  }
45
46
  default:
@@ -87,7 +88,8 @@ export async function runEphemeralScript(workspaceRoot, language, code, options)
87
88
  // Collect all temp files created so we can clean them up unconditionally
88
89
  const tempFiles = [scriptPath];
89
90
  if (language.toLowerCase() === 'rust') {
90
- tempFiles.push(scriptPath.replace('.rs', ''));
91
+ const binExt = os.platform() === 'win32' ? '.exe' : '';
92
+ tempFiles.push(scriptPath.replace('.rs', binExt));
91
93
  }
92
94
  try {
93
95
  await fs.writeFile(scriptPath, code, 'utf-8');
@@ -438,7 +438,7 @@ async function walkWorkspace(workspaceRoot) {
438
438
  .map((l) => l.trim())
439
439
  .filter((l) => l && !l.startsWith('#'));
440
440
  for (const line of lines) {
441
- const clean = line.replace(/^\//, '').replace(/\/$/, '');
441
+ const clean = line.replace(/\\/g, '/').replace(/^\//, '').replace(/\/$/, '');
442
442
  if (!clean.includes('*')) {
443
443
  extraIgnored.add(clean);
444
444
  }
@@ -0,0 +1,2 @@
1
+ export declare const LOGO: string;
2
+ export declare function printLogo(): void;
@@ -0,0 +1,12 @@
1
+ import pc from 'picocolors';
2
+ export const LOGO = `
3
+ ${pc.blue('///')} ${pc.cyan('//')} ${pc.cyan('\\\\')} ${pc.blue('\\\\\\')} ${pc.blue(' __ __ __ __ ____ _ ___ ')}
4
+ ${pc.blue('///')} ${pc.cyan('//')} ${pc.cyan('\\\\')} ${pc.blue('\\\\\\')} ${pc.blue(' | \\/ || \\/ | / ___| | |_ _|')}
5
+ ${pc.blue('///')} ${pc.cyan('//')} ${pc.cyan('●')} ${pc.cyan('\\\\')} ${pc.blue('\\\\\\')} ${pc.blue(' | |\\/| || |\\/| | | | | | | | ')}
6
+ ${pc.blue('\\\\\\')} ${pc.cyan('\\\\')} ${pc.cyan('//')} ${pc.blue('///')} ${pc.blue(' | | | || | | | | |___| |___ | | ')}
7
+ ${pc.blue('\\\\\\')} ${pc.cyan('\\\\')} ${pc.cyan('//')} ${pc.blue('///')} ${pc.blue(' |_| |_||_| |_| \\____|_____|___|')}
8
+ ${pc.blue('\\\\\\')} ${pc.cyan('\\\\')} ${pc.cyan('//')} ${pc.blue('///')}
9
+ `;
10
+ export function printLogo() {
11
+ console.log(pc.bold(LOGO));
12
+ }
@@ -5,6 +5,7 @@ const EXTENSION_MAP = {
5
5
  '.py': 'python', '.pyw': 'python',
6
6
  '.go': 'go',
7
7
  '.rs': 'rust',
8
+ '.php': 'php',
8
9
  };
9
10
  function detectLanguage(filePath) {
10
11
  return EXTENSION_MAP[path.extname(filePath).toLowerCase()] ?? 'unknown';
@@ -26,16 +27,16 @@ function stripStringsAndComments(source, lang) {
26
27
  while (i < source.length) {
27
28
  const ch = source[i];
28
29
  const next = source[i + 1];
29
- // ── JS/TS/Go/Rust: line comments (//) ─────────────────────
30
- if ((lang === 'js' || lang === 'go' || lang === 'rust') && ch === '/' && next === '/') {
30
+ // ── JS/TS/Go/Rust/PHP: line comments (//) ─────────────────────
31
+ if ((lang === 'js' || lang === 'go' || lang === 'rust' || lang === 'php') && ch === '/' && next === '/') {
31
32
  const eol = source.indexOf('\n', i);
32
33
  const end = eol === -1 ? source.length : eol;
33
34
  result += ' '.repeat(end - i);
34
35
  i = end;
35
36
  continue;
36
37
  }
37
- // ── JS/TS/Go/Rust: block comments (/* ... */) ─────────────
38
- if ((lang === 'js' || lang === 'go' || lang === 'rust') && ch === '/' && next === '*') {
38
+ // ── JS/TS/Go/Rust/PHP: block comments (/* ... */) ─────────────
39
+ if ((lang === 'js' || lang === 'go' || lang === 'rust' || lang === 'php') && ch === '/' && next === '*') {
39
40
  const end = source.indexOf('*/', i + 2);
40
41
  const closePos = end === -1 ? source.length : end + 2;
41
42
  // Preserve newlines for line counting
@@ -44,8 +45,8 @@ function stripStringsAndComments(source, lang) {
44
45
  i = closePos;
45
46
  continue;
46
47
  }
47
- // ── Python: line comments (#) ─────────────────────────────
48
- if (lang === 'python' && ch === '#') {
48
+ // ── Python/PHP: line comments (#) ─────────────────────────────
49
+ if ((lang === 'python' || lang === 'php') && ch === '#') {
49
50
  const eol = source.indexOf('\n', i);
50
51
  const end = eol === -1 ? source.length : eol;
51
52
  result += ' '.repeat(end - i);
@@ -62,6 +63,26 @@ function stripStringsAndComments(source, lang) {
62
63
  i = closePos;
63
64
  continue;
64
65
  }
66
+ // ── PHP: Heredoc/Nowdoc (<<<EOF ... EOF;) ─────────────────────
67
+ if (lang === 'php' && ch === '<' && next === '<' && source[i + 2] === '<') {
68
+ const eol = source.indexOf('\n', i);
69
+ if (eol !== -1) {
70
+ // e.g. <<<EOF or <<<'EOF'
71
+ const idMatch = source.substring(i + 3, eol).match(/[A-Za-z0-9_]+/);
72
+ if (idMatch) {
73
+ const identifier = idMatch[0];
74
+ const closePattern = new RegExp(`\\n${identifier}(;|\\n|$)`);
75
+ const match = source.substring(eol).match(closePattern);
76
+ if (match) {
77
+ const end = eol + match.index + match[0].length;
78
+ const segment = source.substring(i, end);
79
+ result += segment.replace(/[^\n]/g, ' ');
80
+ i = end;
81
+ continue;
82
+ }
83
+ }
84
+ }
85
+ }
65
86
  // ── All languages: string literals ────────────────────────
66
87
  if (ch === '"' || ch === "'" || (lang === 'js' && ch === '`')) {
67
88
  const quote = ch;
@@ -119,7 +140,7 @@ const detectNestedLoops = (source, stripped, lang, findings) => {
119
140
  return;
120
141
  }
121
142
  // C-family: scan for loop keywords and track brace depth
122
- const loopPattern = /\b(for|while|do)\s*[\s(]/g;
143
+ const loopPattern = /\b(for|foreach|while|do)\s*[\s(]/g;
123
144
  const lines = stripped.split('\n');
124
145
  for (let i = 0; i < lines.length; i++) {
125
146
  const line = lines[i];
@@ -144,7 +165,7 @@ const detectNestedLoops = (source, stripped, lang, findings) => {
144
165
  }
145
166
  // Check lines inside the outer loop body for nested loops
146
167
  if (j > i && foundOpening && depth > 0) {
147
- if (/\b(for|while|do)\s*[\s(]/.test(innerLine)) {
168
+ if (/\b(for|foreach|while|do)\s*[\s(]/.test(innerLine)) {
148
169
  findings.push({
149
170
  severity: 'WARNING',
150
171
  code: 'PERF-001',
@@ -484,6 +505,21 @@ const detectUnboundedFetch = (_source, stripped, lang, findings) => {
484
505
  }
485
506
  }
486
507
  }
508
+ if (lang === 'php') {
509
+ if (/\.(all|get|findAll|query)\s*\(|::all\s*\(/.test(line)) {
510
+ const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
511
+ if (!/\.(limit|take|paginate|offset)\s*\(/.test(window) &&
512
+ !/\bLIMIT\b/i.test(window)) {
513
+ findings.push({
514
+ severity: 'WARNING',
515
+ code: 'PERF-008',
516
+ line: i + 1,
517
+ message: 'Database/ORM query without limit() or paginate() — may fetch unbounded rows into memory.',
518
+ suggestion: 'Add a limit() or use pagination.',
519
+ });
520
+ }
521
+ }
522
+ }
487
523
  // Go / Rust: detect SQL queries without LIMIT
488
524
  if (lang === 'go' || lang === 'rust') {
489
525
  if (/SELECT\s+/i.test(line) && !/\bLIMIT\b/i.test(line)) {
@@ -502,6 +538,67 @@ const detectUnboundedFetch = (_source, stripped, lang, findings) => {
502
538
  }
503
539
  }
504
540
  };
541
+ // ── PERF-PHP1 / PHP2 / PHP3: PHP-specific loop anti-patterns ───────
542
+ const detectPhpInLoopOperations = (_source, stripped, lang, findings) => {
543
+ if (lang !== 'php')
544
+ return;
545
+ const lines = stripped.split('\n');
546
+ let loopDepth = 0;
547
+ let braceDepth = 0;
548
+ let inLoop = false;
549
+ let loopBraceStart = 0;
550
+ for (let i = 0; i < lines.length; i++) {
551
+ const line = lines[i];
552
+ if (/\b(for|foreach|while|do)\s*[\s(]/.test(line) && !inLoop) {
553
+ inLoop = true;
554
+ loopBraceStart = braceDepth;
555
+ loopDepth++;
556
+ }
557
+ for (const ch of line) {
558
+ if (ch === '{')
559
+ braceDepth++;
560
+ else if (ch === '}') {
561
+ braceDepth--;
562
+ if (inLoop && braceDepth <= loopBraceStart) {
563
+ inLoop = false;
564
+ loopDepth--;
565
+ }
566
+ }
567
+ }
568
+ if (loopDepth > 0) {
569
+ // PERF-PHP1: array_merge
570
+ if (/\barray_merge\s*\(/.test(line)) {
571
+ findings.push({
572
+ severity: 'WARNING',
573
+ code: 'PERF-PHP1',
574
+ line: i + 1,
575
+ message: 'array_merge() inside a loop — causes O(n²) memory inflation and copying.',
576
+ suggestion: 'Append to array with $arr[] = $val and merge once outside the loop, or use the splat operator.',
577
+ });
578
+ }
579
+ // PERF-PHP2: N+1 queries
580
+ if (/(->get\(|->find\(|->query\(|->findAll\(|::find\(|DB::|->execute\()/.test(line)) {
581
+ findings.push({
582
+ severity: 'WARNING',
583
+ code: 'PERF-PHP2',
584
+ line: i + 1,
585
+ message: 'Database query execution inside a loop (N+1 query problem).',
586
+ suggestion: 'Pre-fetch data outside the loop using eager loading (e.g., with()) or a single IN() query.',
587
+ });
588
+ }
589
+ // PERF-PHP3: Blocking I/O
590
+ if (/\b(file_get_contents|curl_exec|fopen)\s*\(/.test(line)) {
591
+ findings.push({
592
+ severity: 'WARNING',
593
+ code: 'PERF-PHP3',
594
+ line: i + 1,
595
+ message: 'Blocking I/O operation inside a loop.',
596
+ suggestion: 'Perform I/O in parallel using curl_multi_exec, or bulk fetch data before the loop.',
597
+ });
598
+ }
599
+ }
600
+ }
601
+ };
505
602
  // ─── Rule Registry ───────────────────────────────────────────────────
506
603
  const ALL_RULES = [
507
604
  detectNestedLoops,
@@ -512,6 +609,7 @@ const ALL_RULES = [
512
609
  detectDangerousEval,
513
610
  detectUnsafeJsonParse,
514
611
  detectUnboundedFetch,
612
+ detectPhpInLoopOperations,
515
613
  ];
516
614
  // ─── Supported Extensions for Auditing ───────────────────────────────
517
615
  const AUDITABLE_EXTENSIONS = new Set(Object.keys(EXTENSION_MAP));
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Gets the path to the project's .minovativemind storage directory.
3
+ */
4
+ export declare function getProjectStorageDir(workspaceRoot: string): string;
5
+ /**
6
+ * Ensures that the .minovativemind directory exists in the workspace root.
7
+ * This is a self-healing directory used for crash-safe state and caching.
8
+ */
9
+ export declare function ensureProjectStorage(workspaceRoot: string): void;
10
+ /**
11
+ * Ensures that the .minovativemind directory is safely ignored from version control,
12
+ * docker images, and npm packages to prevent state leakage.
13
+ */
14
+ export declare function ensureIgnored(workspaceRoot: string): void;
15
+ /**
16
+ * Reads a JSON file from the project's .minovativemind directory.
17
+ * Returns null if the file does not exist or is corrupted.
18
+ */
19
+ export declare function readCache<T = any>(workspaceRoot: string, filename: string): T | null;
20
+ /**
21
+ * Writes data to a JSON file in the project's .minovativemind directory using an atomic write pattern
22
+ * to prevent corruption from concurrent accesses.
23
+ */
24
+ export declare function writeCache(workspaceRoot: string, filename: string, data: any): Promise<void>;
25
+ /**
26
+ * Invalidates the context cache for files that depend on the given modified files.
27
+ * This ensures that if a core file changes, its dependents are forced to generate
28
+ * fresh summaries reflecting the new behavior.
29
+ */
30
+ export declare function invalidateCacheForDependents(workspaceRoot: string, changedFiles: string[]): Promise<void>;
@@ -0,0 +1,157 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as pc from 'picocolors';
4
+ import { buildDependencyGraph } from './dependencyTracer.js';
5
+ /**
6
+ * Gets the path to the project's .minovativemind storage directory.
7
+ */
8
+ export function getProjectStorageDir(workspaceRoot) {
9
+ return path.join(workspaceRoot, '.minovativemind');
10
+ }
11
+ /**
12
+ * Ensures that the .minovativemind directory exists in the workspace root.
13
+ * This is a self-healing directory used for crash-safe state and caching.
14
+ */
15
+ export function ensureProjectStorage(workspaceRoot) {
16
+ const storageDir = getProjectStorageDir(workspaceRoot);
17
+ if (!fs.existsSync(storageDir)) {
18
+ try {
19
+ fs.mkdirSync(storageDir, { recursive: true });
20
+ }
21
+ catch (err) {
22
+ // Non-fatal error, CLI can proceed without cache if permissions fail
23
+ console.warn(pc.yellow(`Failed to create .minovativemind directory: ${err instanceof Error ? err.message : String(err)}`));
24
+ }
25
+ }
26
+ }
27
+ /**
28
+ * Ensures that the .minovativemind directory is safely ignored from version control,
29
+ * docker images, and npm packages to prevent state leakage.
30
+ */
31
+ export function ensureIgnored(workspaceRoot) {
32
+ const ignoreFiles = ['.gitignore', '.dockerignore', '.npmignore', '.eslintignore', '.prettierignore'];
33
+ const gitDirExists = fs.existsSync(path.join(workspaceRoot, '.git'));
34
+ // If .git exists but .gitignore doesn't, we should create a minimal .gitignore
35
+ if (gitDirExists && !fs.existsSync(path.join(workspaceRoot, '.gitignore'))) {
36
+ try {
37
+ fs.writeFileSync(path.join(workspaceRoot, '.gitignore'), '# Minovative CLI Data\n.minovativemind/\n', 'utf-8');
38
+ }
39
+ catch (e) {
40
+ // ignore
41
+ }
42
+ }
43
+ for (const ignoreFile of ignoreFiles) {
44
+ const filePath = path.join(workspaceRoot, ignoreFile);
45
+ // Only modify the file if it actually exists (except for the .gitignore creation above)
46
+ if (fs.existsSync(filePath)) {
47
+ try {
48
+ const content = fs.readFileSync(filePath, 'utf-8');
49
+ // Check if it already ignores .minovativemind or .minovativemind/
50
+ if (!content.includes('.minovativemind')) {
51
+ // Append with a newline just in case the file doesn't end with one
52
+ const appendContent = content.endsWith('\n') || content.length === 0 ? '.minovativemind/\n' : '\n.minovativemind/\n';
53
+ fs.appendFileSync(filePath, appendContent, 'utf-8');
54
+ }
55
+ }
56
+ catch (e) {
57
+ // Silently ignore permissions errors etc.
58
+ }
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * Reads a JSON file from the project's .minovativemind directory.
64
+ * Returns null if the file does not exist or is corrupted.
65
+ */
66
+ export function readCache(workspaceRoot, filename) {
67
+ const filePath = path.join(getProjectStorageDir(workspaceRoot), filename);
68
+ if (!fs.existsSync(filePath))
69
+ return null;
70
+ try {
71
+ const data = fs.readFileSync(filePath, 'utf-8');
72
+ return JSON.parse(data);
73
+ }
74
+ catch (err) {
75
+ // Treat parsing errors as cache misses
76
+ return null;
77
+ }
78
+ }
79
+ /**
80
+ * Writes data to a JSON file in the project's .minovativemind directory using an atomic write pattern
81
+ * to prevent corruption from concurrent accesses.
82
+ */
83
+ export async function writeCache(workspaceRoot, filename, data) {
84
+ ensureProjectStorage(workspaceRoot); // Self-healing check
85
+ const filePath = path.join(getProjectStorageDir(workspaceRoot), filename);
86
+ const tempPath = `${filePath}.${Date.now()}-${Math.floor(Math.random() * 1000)}.tmp`;
87
+ try {
88
+ fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
89
+ // Windows retry logic for EPERM/EBUSY on file replace
90
+ const maxRetries = 3;
91
+ let attempt = 0;
92
+ while (attempt <= maxRetries) {
93
+ try {
94
+ fs.renameSync(tempPath, filePath);
95
+ break; // Success
96
+ }
97
+ catch (err) {
98
+ if (attempt === maxRetries || (err.code !== 'EPERM' && err.code !== 'EBUSY')) {
99
+ throw err;
100
+ }
101
+ attempt++;
102
+ const delay = attempt === 1 ? 10 : attempt === 2 ? 50 : 100;
103
+ await new Promise((resolve) => setTimeout(resolve, delay));
104
+ }
105
+ }
106
+ }
107
+ catch (err) {
108
+ // Non-fatal, clean up temp file if possible
109
+ try {
110
+ if (fs.existsSync(tempPath))
111
+ fs.unlinkSync(tempPath);
112
+ }
113
+ catch { }
114
+ }
115
+ }
116
+ /**
117
+ * Invalidates the context cache for files that depend on the given modified files.
118
+ * This ensures that if a core file changes, its dependents are forced to generate
119
+ * fresh summaries reflecting the new behavior.
120
+ */
121
+ export async function invalidateCacheForDependents(workspaceRoot, changedFiles) {
122
+ if (!changedFiles || changedFiles.length === 0)
123
+ return;
124
+ try {
125
+ const cachedContext = readCache(workspaceRoot, 'context_cache.json');
126
+ if (!cachedContext)
127
+ return;
128
+ let updated = false;
129
+ const graph = await buildDependencyGraph(workspaceRoot);
130
+ for (const file of changedFiles) {
131
+ const reverseDeps = graph.getImportedBy(file);
132
+ for (const dep of reverseDeps) {
133
+ try {
134
+ const content = fs.readFileSync(path.join(workspaceRoot, dep), 'utf-8');
135
+ const crypto = await import('crypto');
136
+ const fileHash = crypto
137
+ .createHash('sha256')
138
+ .update(dep + content)
139
+ .digest('hex');
140
+ if (cachedContext[fileHash]) {
141
+ delete cachedContext[fileHash];
142
+ updated = true;
143
+ }
144
+ }
145
+ catch (e) {
146
+ // If we can't read the dependent file, it might have been deleted. Skip it.
147
+ }
148
+ }
149
+ }
150
+ if (updated) {
151
+ writeCache(workspaceRoot, 'context_cache.json', cachedContext);
152
+ }
153
+ }
154
+ catch (err) {
155
+ console.warn(pc.yellow(`Failed to invalidate cache for dependents: ${err}`));
156
+ }
157
+ }
@@ -65,5 +65,5 @@
65
65
  ]
66
66
  }
67
67
  },
68
- "version": "1.3.0"
68
+ "version": "1.4.0"
69
69
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "minovative-mind-cli",
3
3
  "description": "An automated AI agent powered by Vertex AI that helps you write software",
4
- "version": "1.3.0",
4
+ "version": "1.4.0",
5
5
  "author": "Daniel Ward",
6
6
  "bin": {
7
7
  "minovative-mind-cli": "./bin/run.js"