minovative-mind-cli 2.1.0 → 2.1.2

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
@@ -15,6 +15,9 @@ the build/performance metrics are green.
15
15
  [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)
16
16
  [![Version](https://img.shields.io/npm/v/minovative-mind-cli.svg)](https://npmjs.org/package/minovative-mind-cli)
17
17
  [![Downloads/week](https://img.shields.io/npm/dw/minovative-mind-cli.svg)](https://npmjs.org/package/minovative-mind-cli)
18
+ [![Node.js Version](https://img.shields.io/node/v/minovative-mind-cli.svg)](https://nodejs.org)
19
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
20
+ [![License](https://img.shields.io/npm/l/minovative-mind-cli.svg)](https://github.com/quarantiine/minovative-mind-cli/blob/main/LICENSE.md)
18
21
 
19
22
  - Official Website: [Main Website](https://www.minovativemind.dev/)
20
23
  - Latest Updates: [Updates](https://www.minovativemind.dev/updates)
@@ -112,6 +115,7 @@ Hot-swap during a session using `/models`:
112
115
  | `/commit` | Generate a conventional commit message from your diff |
113
116
  | `/revert` | Undo changes from the last turn, or toggle the revert logger |
114
117
  | `/chats` | View, resume, or delete previous chat sessions |
118
+ | `/workspaces` | Manage active workspaces and linked cross-repo aliases |
115
119
  | `stop` | Abort generation immediately |
116
120
 
117
121
  ---
@@ -361,7 +361,147 @@ export async function readFile(workspaceRoot, filePath, startLine, endLine, targ
361
361
  inlineData: { mimeType: 'application/pdf', data: base64Data },
362
362
  };
363
363
  }
364
+ // Prevent reading massive lockfiles
365
+ const isLockfile = ['package-lock.json', 'yarn.lock', 'poetry.lock', 'pnpm-lock.yaml'].some(file => filePath.toLowerCase().endsWith(file));
366
+ if (isLockfile) {
367
+ return {
368
+ output: '',
369
+ error: `Do not read lockfiles directly as they waste thousands of tokens. Please read the 'package.json', 'pyproject.toml', or equivalent source file to see dependencies.`,
370
+ };
371
+ }
372
+ // Handle SQLite databases natively by dumping the schema
373
+ if (filePath.toLowerCase().endsWith('.sqlite') || filePath.toLowerCase().endsWith('.db')) {
374
+ try {
375
+ const { stdout } = await execAsync(`sqlite3 "${absPath}" ".schema"`);
376
+ return {
377
+ output: `## Database Schema for ${path.basename(absPath)}\n\`\`\`sql\n${stdout}\n\`\`\``,
378
+ };
379
+ }
380
+ catch (e) {
381
+ return {
382
+ output: '',
383
+ error: `Could not parse SQLite DB natively. Ensure 'sqlite3' CLI is installed. Error: ${e instanceof Error ? e.message : String(e)}`,
384
+ };
385
+ }
386
+ }
364
387
  let content = await fs.readFile(absPath, 'utf-8');
388
+ // Handle Jupyter Notebooks natively
389
+ if (filePath.toLowerCase().endsWith('.ipynb')) {
390
+ try {
391
+ const notebook = JSON.parse(content);
392
+ if (notebook && Array.isArray(notebook.cells)) {
393
+ const cleanCells = [];
394
+ for (const cell of notebook.cells) {
395
+ const sourceArray = Array.isArray(cell.source) ? cell.source : [cell.source || ''];
396
+ const source = sourceArray.join('');
397
+ if (cell.cell_type === 'markdown') {
398
+ cleanCells.push(source);
399
+ }
400
+ else if (cell.cell_type === 'code') {
401
+ let cellStr = '```python\n' + source + '\n```';
402
+ if (cell.outputs && Array.isArray(cell.outputs)) {
403
+ const textOutputs = [];
404
+ for (const out of cell.outputs) {
405
+ if (out.output_type === 'stream' && out.text) {
406
+ const text = Array.isArray(out.text) ? out.text.join('') : out.text;
407
+ textOutputs.push(text);
408
+ }
409
+ else if (out.data) {
410
+ if (out.data['text/plain']) {
411
+ const text = Array.isArray(out.data['text/plain']) ? out.data['text/plain'].join('') : out.data['text/plain'];
412
+ textOutputs.push(text);
413
+ }
414
+ else if (out.data['image/png'] || out.data['image/jpeg'] || out.data['image/svg+xml']) {
415
+ textOutputs.push('[Image Output]');
416
+ }
417
+ else if (out.data['text/html']) {
418
+ textOutputs.push('[HTML Output]');
419
+ }
420
+ }
421
+ }
422
+ if (textOutputs.length > 0) {
423
+ let joinedOutput = textOutputs.join('\n').trim();
424
+ const outLines = joinedOutput.split('\n');
425
+ if (outLines.length > 50) {
426
+ joinedOutput = outLines.slice(0, 50).join('\n') + '\n... (Output truncated)';
427
+ }
428
+ cellStr += '\n\n**Output:**\n```text\n' + joinedOutput + '\n```';
429
+ }
430
+ }
431
+ cleanCells.push(cellStr);
432
+ }
433
+ }
434
+ content = cleanCells.join('\n\n');
435
+ }
436
+ }
437
+ catch (err) {
438
+ content = `<!-- Failed to parse .ipynb cleanly, showing raw JSON -->\n${content}`;
439
+ }
440
+ }
441
+ // Handle CSV natively by formatting as a Markdown table
442
+ if (filePath.toLowerCase().endsWith('.csv')) {
443
+ try {
444
+ const rows = [];
445
+ let currentRow = [];
446
+ let currentCell = '';
447
+ let inQuotes = false;
448
+ for (let i = 0; i < content.length; i++) {
449
+ const char = content[i];
450
+ if (char === '"') {
451
+ if (inQuotes && content[i + 1] === '"') {
452
+ currentCell += '"';
453
+ i++;
454
+ }
455
+ else {
456
+ inQuotes = !inQuotes;
457
+ }
458
+ }
459
+ else if (char === ',' && !inQuotes) {
460
+ currentRow.push(currentCell.trim().replace(/\n/g, ' ').replace(/\|/g, '\\|'));
461
+ currentCell = '';
462
+ }
463
+ else if ((char === '\n' || char === '\r') && !inQuotes) {
464
+ if (char === '\r' && content[i + 1] === '\n')
465
+ i++;
466
+ currentRow.push(currentCell.trim().replace(/\n/g, ' ').replace(/\|/g, '\\|'));
467
+ if (currentRow.some(cell => cell.length > 0)) {
468
+ rows.push(currentRow);
469
+ }
470
+ currentRow = [];
471
+ currentCell = '';
472
+ }
473
+ else {
474
+ currentCell += char;
475
+ }
476
+ }
477
+ if (currentCell !== '' || currentRow.length > 0) {
478
+ currentRow.push(currentCell.trim().replace(/\n/g, ' ').replace(/\|/g, '\\|'));
479
+ if (currentRow.some(cell => cell.length > 0))
480
+ rows.push(currentRow);
481
+ }
482
+ if (rows.length > 0) {
483
+ const header = rows[0];
484
+ const MAX_CSV_ROWS = 250; // Generous truncation limit
485
+ const isTruncated = rows.length > MAX_CSV_ROWS;
486
+ const displayRows = isTruncated ? rows.slice(0, MAX_CSV_ROWS) : rows;
487
+ let mdTable = `| ${header.join(' | ')} |\n`;
488
+ mdTable += `| ${header.map(() => '---').join(' | ')} |\n`;
489
+ for (let i = 1; i < displayRows.length; i++) {
490
+ const row = displayRows[i];
491
+ while (row.length < header.length)
492
+ row.push('');
493
+ mdTable += `| ${row.slice(0, header.length).join(' | ')} |\n`;
494
+ }
495
+ if (isTruncated) {
496
+ mdTable += `\n*(Note: CSV truncated from ${rows.length} rows to ${MAX_CSV_ROWS} rows for safe viewing)*\n`;
497
+ }
498
+ content = mdTable;
499
+ }
500
+ }
501
+ catch (err) {
502
+ content = `<!-- Failed to parse .csv as table, showing raw text -->\n${content}`;
503
+ }
504
+ }
365
505
  if (targetElements && targetElements.length > 0) {
366
506
  content = extractSymbols(content, filePath, targetElements);
367
507
  }
@@ -4,7 +4,7 @@ import pc from 'picocolors';
4
4
  import { createContextAgentSession, createIntentRouterSession, createWebSearchAgentSession, createExecutionComplexitySession, } from './ai.js';
5
5
  import { evaluateInvestigationComplexity } from './investigationComplexity.js';
6
6
  import { InvestigationOrchestrator } from './orchestration/investigationOrchestrator.js';
7
- import { isSubAgentsEnabled, listDirectory, grepSearch, readFile, traceDependencies, findRecentChanges } from './agent-tools.js';
7
+ import { isSubAgentsEnabled, executeTool } from './agent-tools.js';
8
8
  import { debugLog } from '../utils/logger.js';
9
9
  import { buildDependencyGraph } from '../utils/dependencyTracer.js';
10
10
  import { runEphemeralScript } from '../utils/analysisRunner.js';
@@ -198,7 +198,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
198
198
  let primaryProjectType = 'Unknown';
199
199
  for (const { alias, root } of allRoots) {
200
200
  const label = alias ? `@${alias} (${root})` : `Primary Workspace (${root})`;
201
- const treeResult = await listDirectory(root, '.', 10);
201
+ const treeResult = await executeTool(root, 'list_directory', { dirPath: '.', maxDepth: 10 });
202
202
  let tree = treeResult.output;
203
203
  if (tree.length > 30000) {
204
204
  tree = tree.substring(0, 30000) + '\\n... (Project tree truncated due to size)';
@@ -328,7 +328,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
328
328
  debugLog(`Context Agent finished. Selected files: ${JSON.stringify(filesToRead)}`);
329
329
  for (const filePath of filesToRead) {
330
330
  if (!relevantFiles.has(filePath)) {
331
- const readResult = await readFile(workspaceRoot, filePath);
331
+ const readResult = await executeTool(workspaceRoot, 'read_file', { filePath });
332
332
  if (!readResult.error) {
333
333
  relevantFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
334
334
  }
@@ -342,15 +342,23 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
342
342
  // Execution Agent won't break imports when modifying/deleting/renaming.
343
343
  const MAX_TOTAL_FILES = 15;
344
344
  try {
345
- const graph = await buildDependencyGraph(workspaceRoot);
345
+ const { resolveAndValidateMultiWorkspacePath } = await import('../utils/pathSecurity.js');
346
346
  const autoDiscovered = new Set();
347
347
  for (const filePath of filesToRead) {
348
- const reverseDeps = graph.getImportedBy(filePath);
349
- for (const dep of reverseDeps) {
350
- if (!filesToRead.includes(dep) && !autoDiscovered.has(dep)) {
351
- autoDiscovered.add(dep);
348
+ try {
349
+ const resolved = resolveAndValidateMultiWorkspacePath(workspaceRoot, filePath);
350
+ const graph = await buildDependencyGraph(resolved.workspaceRoot);
351
+ const reverseDeps = graph.getImportedBy(resolved.relativePath);
352
+ for (const dep of reverseDeps) {
353
+ const aliasedDep = resolved.alias ? `@${resolved.alias}/${dep}` : dep;
354
+ if (!filesToRead.includes(aliasedDep) && !autoDiscovered.has(aliasedDep)) {
355
+ autoDiscovered.add(aliasedDep);
356
+ }
352
357
  }
353
358
  }
359
+ catch (e) {
360
+ // Ignore path resolution errors for trace dependencies
361
+ }
354
362
  }
355
363
  // Merge auto-discovered dependents, respecting the file cap
356
364
  const remaining = MAX_TOTAL_FILES - relevantFiles.size;
@@ -359,7 +367,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
359
367
  if (added >= remaining)
360
368
  break;
361
369
  if (!relevantFiles.has(dep)) {
362
- const readResult = await readFile(workspaceRoot, dep);
370
+ const readResult = await executeTool(workspaceRoot, 'read_file', { filePath: dep });
363
371
  if (!readResult.error) {
364
372
  relevantFiles.set(dep, { text: readResult.output, inlineData: readResult.inlineData });
365
373
  added++;
@@ -389,7 +397,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
389
397
  const filesToRead = args.files || [];
390
398
  let output = '';
391
399
  for (const filePath of filesToRead) {
392
- const readResult = await readFile(workspaceRoot, filePath);
400
+ const readResult = await executeTool(workspaceRoot, 'read_file', { filePath });
393
401
  if (!readResult.error) {
394
402
  relevantFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
395
403
  output += `\n--- File: ${filePath} ---\n${readResult.output}\n`;
@@ -406,7 +414,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
406
414
  });
407
415
  }
408
416
  else if (call.name === 'list_directory') {
409
- const listRes = await listDirectory(workspaceRoot, args.dirPath, args.maxDepth || 1);
417
+ const listRes = await executeTool(workspaceRoot, 'list_directory', args);
410
418
  functionResponses.push({
411
419
  functionResponse: {
412
420
  name: call.name,
@@ -418,7 +426,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
418
426
  });
419
427
  }
420
428
  else if (call.name === 'search_codebase') {
421
- const grepRes = await grepSearch(workspaceRoot, args.pattern, args.fileGlob);
429
+ const grepRes = await executeTool(workspaceRoot, 'grep_search', { ...args, workspace: 'all' });
422
430
  functionResponses.push({
423
431
  functionResponse: {
424
432
  name: call.name,
@@ -427,44 +435,16 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
427
435
  });
428
436
  }
429
437
  else if (call.name === 'semantic_search') {
430
- const query = args.query;
431
- const topK = args.topK || 5;
432
- let output = '';
433
- try {
434
- const { getEmbeddingIndex } = await import('./embeddingIndex.js');
435
- const index = getEmbeddingIndex();
436
- if (!index.isReady()) {
437
- // Lazy load from disk or build if missing
438
- const loaded = await index.load(workspaceRoot);
439
- if (!loaded) {
440
- if (onProgress)
441
- onProgress('Building semantic search index (first run)...');
442
- await index.buildIndex(workspaceRoot, onProgress);
443
- await index.save(workspaceRoot);
444
- }
445
- }
446
- const results = await index.search(query, topK);
447
- if (results.length === 0) {
448
- output = 'No semantically similar code found. (Index might be empty or embedding failed)';
449
- }
450
- else {
451
- output = results
452
- .map((r) => `[Score: ${r.score.toFixed(3)}] ${r.filePath}:${r.startLine}-${r.endLine}\n${r.preview}`)
453
- .join('\n---\n');
454
- }
455
- }
456
- catch (e) {
457
- output = `Semantic search failed: ${e.message}`;
458
- }
438
+ const semRes = await executeTool(workspaceRoot, 'semantic_search', args);
459
439
  functionResponses.push({
460
440
  functionResponse: {
461
441
  name: call.name,
462
- response: { output },
442
+ response: { output: semRes.output },
463
443
  },
464
444
  });
465
445
  }
466
446
  else if (call.name === 'read_file') {
467
- const readRes = await readFile(workspaceRoot, args.filePath, args.startLine, args.endLine, args.targetElements);
447
+ const readRes = await executeTool(workspaceRoot, 'read_file', args);
468
448
  if (!readRes.error) {
469
449
  relevantFiles.set(args.filePath, { text: readRes.output, inlineData: readRes.inlineData });
470
450
  }
@@ -506,7 +486,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
506
486
  }
507
487
  }
508
488
  else if (call.name === 'find_dependencies') {
509
- const depResult = await traceDependencies(workspaceRoot, args.filePath, args.direction, args.maxDepth);
489
+ const depResult = await executeTool(workspaceRoot, 'find_dependencies', args);
510
490
  functionResponses.push({
511
491
  functionResponse: {
512
492
  name: call.name,
@@ -515,7 +495,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
515
495
  });
516
496
  }
517
497
  else if (call.name === 'find_recent_changes') {
518
- const recentRes = await findRecentChanges(workspaceRoot, args.dirPath, args.minutes, args.maxDepth);
498
+ const recentRes = await executeTool(workspaceRoot, 'find_recent_changes', args);
519
499
  functionResponses.push({
520
500
  functionResponse: {
521
501
  name: call.name,
@@ -65,5 +65,5 @@
65
65
  ]
66
66
  }
67
67
  },
68
- "version": "2.1.0"
68
+ "version": "2.1.2"
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": "2.1.0",
4
+ "version": "2.1.2",
5
5
  "author": "Daniel Ward",
6
6
  "bin": {
7
7
  "minovative-mind-cli": "bin/run.js"