Repository: BrodaNoel/cmd-ai
Pull request #9: New provider (Gemini), Multiline cmd execution, explain mode
Author: login: cacuscacus
Created: 2025-05-09T18:58:54Z | Merged: 2025-05-12T22:32:24Z

- FEATURE: New model provider (gemini)
- FEATURE: Multiline command execution
- Fixed the explain mode
- Added error handling in several parts of the code
- Normalized the isDangerous logic
- Added new shell type (ksh)
- Minor fix to download progress calculation and loading/unloading state management for local models
- Generalized the model output parsing
- Added OS architecture to the prompt for better context
- Minor extra code cleanups
- ignoring vscode settings json and package lock


Diff:
diff --git a/.gitignore b/.gitignore
index 30bc162..572406b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-/node_modules
\ No newline at end of file
+/node_modules
+package-lock.json
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3de4a16..84c737f 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,5 +1,5 @@
 {
-  "files.associations": {
+"files.associations": {
     "*.json": "json"
   },
   "editor.formatOnSave": true,
diff --git a/README.md b/README.md
index 3d24b9e..4e06d49 100644
--- a/README.md
+++ b/README.md
@@ -26,9 +26,12 @@ ai config
 
 This command will guide you through choosing between the local (default) and openai providers.
 
-- **local**: Uses the onnx-community/Qwen3-0.6B-ONNX model running directly on your machine. No external API key is required. When you select the local provider for the first time using ai config, the necessary model files (approximately 500MB) will be automatically downloaded with a progress indicator. These files are cached locally for future use.
+- **local**: Uses the onnx-community/Qwen3-0.6B-ONNX model running directly on your machine. No external API key is required. When you select the local provider for the first time using ai config, the necessary model files (approximately 300MB for the model and 2GB for the ONNX data) will be automatically downloaded with a progress indicator. These files are cached locally for future use.
+- **gemini**: Uses the Google Gemini API (`gemini-1.5-flash-latest`). You will need to provide your Google AI Studio API key.
 - **openai**: Uses the OpenAI API. You will need to provide your OpenAI API key for this option.
 
+The default provider is **local**.
+
 Your configuration is stored securely in:
 ```bash
 ~/.ai-config.json
diff --git a/bin/ai.js b/bin/ai.js
index 06a695b..45db7ff 100755
--- a/bin/ai.js
+++ b/bin/ai.js
@@ -13,6 +13,7 @@ const CONFIG_PATH = path.join(os.homedir(), '.ai-config.json');
 const HISTORY_PATH = path.join(os.homedir(), '.ai-command-history.json');
 const LOCAL_MODEL_ID = 'onnx-community/Qwen3-0.6B-ONNX';
 let localPipeline = null;
+let localPipelineStatus = 'unloaded'; // 'unloaded', 'loading', 'loaded', 'error', 'downloading'
 
 const rl = readline.createInterface({
   input: process.stdin,
@@ -29,17 +30,36 @@ function saveHistory(entry) {
   let history = [];
   if (fs.existsSync(HISTORY_PATH)) {
     try {
-      history = JSON.parse(fs.readFileSync(HISTORY_PATH, 'utf-8'));
-    } catch (error) {
-      console.error('🚨 Error saving the command history:');
-      console.error(error);
+      // Limit history size to prevent huge files
+      history = JSON.parse(fs.readFileSync(HISTORY_PATH, 'utf-8')).slice(-1000); // Keep last 1000 entries
+    } catch (e) {
+      console.error('Error reading history file, starting fresh:', e.message);
+      history = [];
     }
   }
   history.push({ ...entry, timestamp: new Date().toISOString() });
-  fs.writeFileSync(HISTORY_PATH, JSON.stringify(history, null, 2));
+  try {
+    fs.writeFileSync(HISTORY_PATH, JSON.stringify(history, null, 2));
+  } catch (e) {
+    console.error('Error writing history file:', e.message);
+  }
 }
 
 function isDangerous(command) {
+  // Normalize command for safer checking
+  const normalized = command
+    .toLowerCase()
+    .replace(/\s+/g, ' ') // normalize spacing
+    .replace(/\\+$/, '') // remove trailing backslashes used for line continuation, check single lines
+    .replace(/\n/g, ';'); // treat newlines like semicolons for splitting
+
+  // Split into potential subcommands (separated by ;, &&, ||)
+  // Basic split might not handle complex quoting/escaping perfectly, but good for common cases
+  const subcommands = normalized
+    .split(/;|&&|\|\||\(|\)|\{|\}/)
+    .map(s => s.trim())
+    .filter(s => s !== '');
+
   const blackListPatterns = [
     'rm -rf /',
     'rm -rf ~',
@@ -52,40 +72,51 @@ function isDangerous(command) {
     'mkfs.xfs',
     'mkfs.vfat',
     'dd if=',
+    'dd of=/dev/', // More specific dd pattern
     ':(){:|:&};:',
-    '>:()',
+    '>:()', // Fork bomb variants
     'shutdown',
     'reboot',
     'halt',
     'poweroff',
     'init 0',
     'init 6',
-    'kill -9 1',
-    'mv /',
+    'kill -9 1', // Killing init/systemd
+    'mv /', // Moving root
     'chmod 000',
-    'chmod -R 000 /',
-    'chown root',
-    'yes > /dev/sda',
-    'yes > /dev/null',
-    '>/dev/sda',
-    '>/dev/null',
-    'mount -o bind / /dev/null',
-    'crontab -r',
-    'echo .* >',
-    'cat /dev/urandom >',
+    'chmod -r 000 /',
+    'chown root', // Changing ownership of critical files/dirs
+    'yes > /dev/', // Overwriting devices
+    '>/dev/', // Overwriting devices
+    'mount -o bind / /dev/null', // Potential FS manipulation
+    'crontab -r', // Removing all cron jobs
+    'echo .* >', // Dangerous output redirection
+    'cat /dev/urandom >', // Dangerous output redirection
     'find / -exec rm',
-    'find / -delete',
+    'find / -delete', // Dangerous find commands
+    'wipefs',
+    'shred', // Data destruction tools
+    'nohup .* >/dev/null 2>&1 &', // Running commands detached and discarding output (can hide malicious activity) - maybe too broad? Let's skip for now.
+    'curl .* | sh',
+    'wget .* | sh', // Downloading and piping to shell
+    'base64 -d <<< .* | sh', // Decoding and piping to shell
   ];
 
-  const normalized = command
-    .toLowerCase()
-    .replace(/\s+/g, ' ') // normalize spacing
-    .replace(/\n/g, ';'); // treat newlines like semicolons
-
-  const subcommands = normalized.split(/;|\&\&|\|\|/).map(s => s.trim());
+  // Basic check for pipe to sh/bash/etc. or similar execution
+  if (
+    /\s*\|\s*(sh|bash|zsh|csh|ksh|python|perl|ruby)\s*(-c|\s|$)/.test(
+      normalized
+    )
+  ) {
+    console.warn(
+      'Potential dangerous pattern: Piping output to a shell or interpreter.'
+    );
+    return true; // Flag as dangerous
+  }
 
   return subcommands.some(sub => {
-    return blackListPatterns.some(pattern => sub.includes(pattern));
+    // Check if subcommand starts with a dangerous pattern
+    return blackListPatterns.some(pattern => sub.startsWith(pattern));
   });
 }
 
@@ -96,6 +127,7 @@ Usage: ai [prompt or command] [--flags]
 Examples:
   ai list files in current directory
   ai remove all docker containers
+  ai list files in current directory and save to file.txt
   ai config                    Set your AI provider (local or openai) and API key
   ai history                   Show history of AI-generated commands
   ai man / --help / -h         Show this help message
@@ -108,6 +140,7 @@ Flags:
 
 Providers:
   local       Uses the Qwen3-0.6B ONNX model running locally. (Default)
+  gemini      Uses the Google Gemini API (requires API key).
   openai      Uses the OpenAI API (requires API key).
 
 Autocomplete:
@@ -121,237 +154,523 @@ function installAutocompleteScript() {
   const targetPath = path.join(os.homedir(), '.cmd-ai-completion.sh');
 
   if (!fs.existsSync(sourcePath)) {
-    console.error(`Autocomplete script not found at: ${sourcePath}`);
+    console.error(`Autocomplete script not found at: ${sourcePath}\n`);
+    console.error(
+      `Please ensure you are running 'ai install-autocomplete' from the directory where cmd-ai was installed globally, or locate the 'cmd-ai-completion.sh' script manually.\n`
+    );
     process.exit(1);
   }
 
   // Copy script to home directory
-  fs.copyFileSync(sourcePath, targetPath);
-  fs.chmodSync(targetPath, 0o644);
-  console.log(`✅ Autocomplete script copied to: ${targetPath}`);
+  try {
+    fs.copyFileSync(sourcePath, targetPath);
+    fs.chmodSync(targetPath, 0o644); // Read/write for user, read for others
+    console.log(`✅ Autocomplete script copied to: ${targetPath}`);
+  } catch (e) {
+    console.error(`Error copying autocomplete script: ${e.message}\n`);
+    process.exit(1);
+  }
 
   // Detect shell config file
   const shell = process.env.SHELL || '';
-  const rcFile = shell.includes('zsh')
-    ? path.join(os.homedir(), '.zshrc')
-    : shell.includes('bash')
-    ? path.join(os.homedir(), '.bashrc')
-    : null;
+  let rcFile = null;
+  if (shell.includes('zsh')) {
+    rcFile = path.join(os.homedir(), '.zshrc');
+  } else if (shell.includes('bash')) {
+    rcFile = path.join(os.homedir(), '.bashrc');
+  } else if (shell.includes('ksh')) {
+    rcFile = path.join(os.homedir(), '.kshrc');
+  }
 
   const sourceCmd = `source ${targetPath}`;
 
   if (rcFile) {
-    const rcContent = fs.existsSync(rcFile)
-      ? fs.readFileSync(rcFile, 'utf-8')
-      : '';
-    if (!rcContent.includes(sourceCmd)) {
-      fs.appendFileSync(rcFile, `\n# cmd-ai autocomplete\n${sourceCmd}\n`);
-      console.log(`✅ Updated ${rcFile} to include autocomplete.`);
-    } else {
-      console.log(`ℹ️ ${rcFile} already includes the autocomplete script.`);
-    }
+    try {
+      const rcContent = fs.existsSync(rcFile)
+        ? fs.readFileSync(rcFile, 'utf-8')
+        : '';
+
+      if (!rcContent.includes(sourceCmd)) {
+        // Check if the source command exists line-by-line or within commented sections
+        const lines = rcContent.split('\n');
+        const alreadySourced = lines.some(line =>
+          line.trim().replace(/^#\s*/, '').includes(sourceCmd)
+        );
 
-    console.log('\nℹ️ Please restart your terminal or run:');
-    console.log(`   source ${rcFile}\n`);
+        if (!alreadySourced) {
+          fs.appendFileSync(rcFile, `\n# cmd-ai autocomplete\n${sourceCmd}\n`);
+          console.log(`✅ Updated ${rcFile} to include autocomplete.`);
+        } else {
+          console.log(
+            `ℹ️ ${rcFile} already includes the autocomplete script (or a commented version).`
+          );
+        }
+      } else {
+        console.log(`ℹ️ ${rcFile} already includes the autocomplete script.`);
+      }
+
+      console.log('\nℹ️ Please restart your terminal or run:');
+      console.log(`   source ${rcFile}\n`);
+    } catch (e) {
+      console.error(
+        `Error updating shell config file ${rcFile}: ${e.message}\n`
+      );
+      console.log('\n🚨 Could not update shell config file automatically.');
+      console.log(
+        `Please manually add this line to your shell config (${rcFile} or similar):`
+      );
+      console.log(`   ${sourceCmd}\n`);
+    }
   } else {
     console.log('\n🚨 Could not detect shell config file automatically.');
-    console.log(`Please manually add this line to your shell config:`);
-    console.log(`   source ${targetPath}\n`);
+    console.log(
+      `Please manually add this line to your shell config (.bashrc, .zshrc, etc.):`
+    );
+    console.log(`   ${sourceCmd}\n`);
   }
 }
 
 // Callback function for download progress
 function downloadProgressCallback({ file, progress, total }) {
-    if (progress && total) {
-        const percentage = Math.round(progress / total * 100);
-        if (percentage > lastProgress) {
-            process.stdout.clearLine(0);
-            process.stdout.cursorTo(0);
-            const fileName = file ? path.basename(file) : 'Model file';
-            process.stdout.write(`Downloading ${fileName}: ${percentage}%\n`);
-            lastProgress = percentage;
-        }
-        if (percentage === 100) {
-            process.stdout.write('\n');
-            lastProgress = 0;
-        }
+  if (progress !== undefined && total !== undefined) {
+    // Check for undefined as progress might be NaN/Infinity sometimes
+    const percentage = Math.round((progress / total) * 100);
+    if (percentage > lastProgress || percentage === 0 || percentage === 100) {
+      process.stdout.clearLine(0);
+      process.stdout.cursorTo(0);
+      const fileName = file ? path.basename(file) : 'Model file';
+      process.stdout.write(`Downloading ${fileName}: ${percentage}%\n`);
+      lastProgress = percentage;
+    }
+    if (percentage === 100 && lastProgress === 100) {
+      process.stdout.write('\n');
+      lastProgress = 0;
     }
+  } else if (file) {
+    process.stdout.clearLine(0);
+    process.stdout.cursorTo(0);
+    process.stdout.write(`Downloading ${path.basename(file)}...\n`);
+  }
 }
 
 // Function to download the local model explicitly
 async function downloadLocalModel() {
-    console.log(`\nInitiating download for local model "${LOCAL_MODEL_ID}"...`);
-    try {
-        // Use pipeline to trigger download. It will cache the model files.
-        // We don't need to assign it to localPipeline here, just trigger the download.
-        await pipeline('text-generation', LOCAL_MODEL_ID, { dtype: "fp32" }, {
-            progress_callback: downloadProgressCallback
-        });
-        console.log('Model download complete.');
-    } catch (error) {
-        console.error('\nError during model download:', error);
-        throw new Error('Local model download failed. Please check your internet connection and disk space.');
+  if (
+    localPipelineStatus === 'loaded' ||
+    localPipelineStatus === 'loading' ||
+    localPipelineStatus === 'downloading'
+  ) {
+    console.log('Local model is already loaded, loading, or downloading.');
+    return;
+  }
+  localPipelineStatus = 'downloading';
+  console.log(`\nInitiating download for local model "${LOCAL_MODEL_ID}"...`);
+  console.log('Files will be cached for future use.');
+  try {
+    // Use pipeline to trigger download. It will cache the model files.
+    // We don't need to assign it to localPipeline here, just trigger the download.
+    await pipeline(
+      'text-generation',
+      LOCAL_MODEL_ID,
+      { dtype: 'fp32' },
+      {
+        progress_callback: downloadProgressCallback,
+      }
+    );
+    console.log('Model download complete.');
+    localPipelineStatus = 'unloaded';
+  } catch (error) {
+    console.error('\nError during model download:', error);
+    localPipelineStatus = 'error';
+    throw new Error(
+      'Local model download failed. Please check your internet connection, disk space, and permissions.'
+    );
+  }
+}
+
+// --- Helper function to parse model output ---
+function parseModelOutput(output, explainMode) {
+  let explanation = null;
+  let command = output.trim(); // Start with the whole output
+
+  // Regex to find fenced code blocks (e.g., ```bash...```)
+  // Capture group 1 is the content inside. Use 's' flag for . to match newlines.
+  const fencedCodeBlockRegex = /```(?:\w+)?\s*([\s\S]+?)```/s;
+
+  const fencedMatch = command.match(fencedCodeBlockRegex);
+
+  if (fencedMatch) {
+    // Found a fenced code block
+    command = fencedMatch[1].trim(); // The content inside the block is the command
+    // Everything before the first block is potentially explanation
+    const explanationPart = output.substring(0, fencedMatch.index).trim();
+    if (explanationPart) {
+      explanation = explanationPart;
+    }
+
+    // Basic cleanup of leading/trailing quotes/backticks/whitespace from command
+    // Although code blocks usually don't have this, being defensive
+    command = command.replace(/^['"`\s]+/, '').replace(/['"`\s]+$/, '');
+
+    // Remove leading conversational filler from explanation part
+    if (explanation) {
+      const conversationalStarts =
+        /^(?:(hi|hello|hey|greetings|i am|i'm|as a large language model|i cannot|i'm sorry|i understand|okay|sure|alright|of course|you can|you could|to do that|here is|here's)|[^\s]+:)/i;
+      const explanationLines = explanation
+        .split('\n')
+        .filter(line => line.trim() !== '');
+      let cleanedExplanationLines = [];
+      for (const line of explanationLines) {
+        if (conversationalStarts.test(line.trim())) {
+          // Stop if a conversational line is encountered
+          break;
+        }
+        cleanedExplanationLines.push(line);
+      }
+      explanation = cleanedExplanationLines.join('\n').trim();
+      if (explanation === '') explanation = null; // If cleanup resulted in empty string
+    }
+  } else {
+    // No fenced code block found. Fallback to line-by-line analysis.
+    // Use a more robust check for command start, including symbols like >, |
+    const commandStartRegex =
+      /^[a-zA-Z0-9_-]+|^\.|\/|~|^[>|!$%&*+,-./:;=?@^_~]/; // Starts with word char, ., /, ~, or common shell symbols/operators
+    const conversationalLineRegex =
+      /^(?:(hi|hello|hey|greetings|i am|i'm|as a large language model|i cannot|i'm sorry|i understand|okay|sure|alright|of course|you can|you could|to do that|here is|here's)|[^\s]+:)/i;
+
+    const lines = output.split('\n'); // Keep empty lines for line numbering context in slicing
+
+    let firstCommandLineIndex = -1;
+    for (let i = 0; i < lines.length; i++) {
+      const trimmedLine = lines[i].trim();
+      if (trimmedLine === '') continue; // Skip empty lines for this check
+
+      // Find the first line that looks like a command and not conversation
+      if (
+        commandStartRegex.test(trimmedLine) &&
+        !conversationalLineRegex.test(trimmedLine.toLowerCase())
+      ) {
+        firstCommandLineIndex = i;
+        break;
+      }
+    }
+
+    if (firstCommandLineIndex !== -1) {
+      // Found a line that looks like a command start
+      if (explainMode) {
+        // In explain mode, lines before are explanation, that line + all subsequent are command
+        explanation = lines.slice(0, firstCommandLineIndex).join('\n').trim();
+        if (explanation === '') explanation = null;
+
+        command = lines.slice(firstCommandLineIndex).join('\n').trim(); // Join all lines from the first command line onwards
+      } else {
+        // In non-explain mode, just take the first identified command line and *all* subsequent lines.
+        // This handles `cmd1\ncmd2` outside a block in non-explain mode.
+        command = lines.slice(firstCommandLineIndex).join('\n').trim();
+        explanation = null; // No explanation extracted in this mode/fallback
+      }
+
+      // Basic cleanup of leading/trailing quotes/backticks/whitespace from command
+      command = command.replace(/^['"`\s]+/, '').replace(/['"`\s]+$/, '');
+      // Remove leading/trailing common shell prompts if model included them
+      command = command.replace(/^\$\s+/, '').replace(/^#\s+/, '');
+    } else {
+      // No line found that looks like a command start.
+      // This might happen if the model is overly conversational or gave an error/unparseable response.
+      console.warn(
+        'Warning: Could not identify a clear command start line or code block in the model output.'
+      );
+      // In this case, return the full output as the command, and no explanation separation was possible.
+      command = output.trim(); // Use the original trimmed output
+      explanation = null; // No clear explanation/command separation possible
     }
+  }
+
+  // Final check on command - if extraction resulted in empty string, fallback to full output
+  if (command === '') {
+    console.warn(
+      'Warning: Command extraction resulted in an empty string. Using full output as command.'
+    );
+    command = output.trim();
+    explanation = null; // Reset explanation as separation failed
+  }
+
+  // Final cleanup of explanation
+  if (explanation) {
+    explanation = explanation.trim();
+    if (explanation === '') explanation = null;
+  }
+
+  return { explanation, command };
 }
 
+// Modify generateCommandLocal to return raw output string
+async function generateCommandLocal(
+  userPrompt,
+  osInfo,
+  shellInfo,
+  explainMode
+) {
+  if (localPipelineStatus === 'error') {
+    throw new Error(
+      'Local model is in an error state. Please re-configure using "ai config".'
+    );
+  }
+  if (localPipelineStatus === 'downloading') {
+    console.log('Waiting for local model download to complete...');
+    // Simple loop to wait, potentially add timeout
+    while (localPipelineStatus === 'downloading') {
+      await new Promise(resolve => setTimeout(resolve, 1000));
+    }
+    if (localPipelineStatus === 'error') {
+      throw new Error('Local model download failed during wait.');
+    }
+    if (localPipelineStatus === 'unloaded') {
+      // Download finished, now it needs loading
+      // continue to loading logic below
+    } else if (localPipelineStatus === 'loaded') {
+      // Download finished and maybe something else loaded it? Unlikely but safe.
+      // continue to inference
+    }
+  }
 
-async function generateCommandLocal(userPrompt, osInfo, explainMode) {
   if (!localPipeline) {
-    console.log(`\nLoading local model "${LOCAL_MODEL_ID}"... (This may take a moment on first load)`);
+    localPipelineStatus = 'loading';
+    console.log(
+      `\nLoading local model "${LOCAL_MODEL_ID}"... (This may take a moment on first load)`
+    );
     try {
-      localPipeline = await pipeline('text-generation', LOCAL_MODEL_ID, { dtype: "fp32" }, {
-           progress_callback: downloadProgressCallback
-      });
+      localPipeline = await pipeline(
+        'text-generation',
+        LOCAL_MODEL_ID,
+        { dtype: 'fp32' },
+        {
+          progress_callback: downloadProgressCallback,
+        }
+      );
       console.log('Model loaded successfully.');
+      localPipelineStatus = 'loaded';
     } catch (error) {
       console.error('\nError loading local model:', error);
-      localPipeline = 'error';
+      localPipelineStatus = 'error';
+      localPipeline = null;
       throw new Error('Local model loading or initialization failed.');
     }
   }
 
-  if (localPipeline === 'error') {
-      throw new Error('Local model is not available or failed to load.');
-  }
-
-  // Qwen3 instruct format is typically ChatML: 
user\nPrompt
\n
assistant\n
-  // We instruct it to act as a shell assistant and output only the command.
-  const systemMessage = `/no_think You are a shell assistant.
-OS: ${osInfo}
-${explainMode ? 'Explain what the command does, then return only the command.\n' : ''}Respond only with safe and correct shell command(s), no commentary or headings.
-Output *only* the command required to perform the task.`;
+  // We instruct it to act as a shell assistant. /no_think is a special instruction to avoid thinking for qwen3 models.
+  // The instruction now asks it to explain *then* provide the command, optionally in a code block.
+  const systemMessage = `
+/no_think You are a helpful shell (${shellInfo}) assistant, running on ${osInfo} OS.
+The user will ask for a task he wants to accomplish or needs help with.
+For example: "List all files in this folder", "Remove all docker containers", "List files in current directory and save to file.txt", etc.
+Your goal is to provide the user with a safe and correct shell command(s) to accomplish the task.
+${
+  explainMode
+    ? 'First, provide a brief explanation of the command. Then, provide the command, ideally in a fenced code block (e.g., ```bash\n...\n```).\n'
+    : 'Respond only with the command, ideally in a fenced code block (e.g., ```bash\n...\n```). No commentary or headings.'
+}
+Output *only* the explanation and the command, or just the command.`; // Added instruction for code block
 
   const prompt = [
-    { role: "system", content: systemMessage },
-    { role: "user", content: userPrompt },
+    { role: 'system', content: systemMessage },
+    { role: 'user', content: userPrompt },
   ];
 
   try {
     const output = await localPipeline(prompt, {
-      max_new_tokens: 200,
+      max_new_tokens: explainMode ? 600 : 150, // Allow more tokens for explanation
       temperature: 0.3,
     });
 
-    let response = output[0]?.generated_text[2].content
+    let response = output[0]?.generated_text[2].content;
     response = response.replace(/<think>[\s\S]*?<\/think>/g, '');
 
     let generatedText = response || '';
 
-    // Sometimes models might repeat the prompt or add conversational filler,
-    // a simple heuristic is to look for the first line that looks like a command.
-    const lines = generatedText.split('\n');
-    let command = generatedText; // Default to the whole output
-
-    // Look for a line starting with a common command or symbol
-    // More robust check: look for a line that *doesn't* look like conversation
-    const conversationalLineRegex = /^(?:(hi|hello|hey|greetings|i am|i'm|as a large language model|i cannot|i'm sorry|i understand|okay|sure|alright|of course|you can|you could|to do that|here is|here's)|[^\s]+:)/i; // Add more conversational starts, check for role-like colons
-    const commandStartRegex = /^\s*([a-zA-Z0-9_-]+|\.|\/|~)/; // Original command start regex
-
-    for (const line of lines) {
-        const trimmedLine = line.trim();
-        if (trimmedLine === '') continue; // Skip empty lines
-
-        // If explain mode is on, the command might be in a code block after explanation.
-        if (explainMode && trimmedLine.startsWith('`')) {
-             // Simple code block detection (might need more robust parsing for multi-line blocks)
-             const codeBlockMatch = trimmedLine.match(/^`+([\s\S]*?)`+$/);
-             if (codeBlockMatch && codeBlockMatch[1]) {
-                 command = codeBlockMatch[1].trim();
-                 break; // Found a code block, assume it's the command
-             }
-        }
+    return generatedText;
+  } catch (error) {
+    console.error('Error generating command with local model:', error);
+    // Check if the error suggests model file issues
+    if (
+      error.message.includes('Not a valid file') ||
+      error.message.includes('Error loading model')
+    ) {
+      console.error(
+        'It seems the local model files are missing or corrupt. Try running "ai config" to re-download.'
+      );
+      localPipelineStatus = 'error'; // Indicate a potential persistent issue
+      localPipeline = null;
+    }
+    throw new Error('Local model inference failed.');
+  }
+}
 
-        // If not in a code block or explain mode, look for a line that looks like a command
-        // and doesn't look like conversational filler.
-        if (commandStartRegex.test(trimmedLine) && !conversationalLineRegex.test(trimmedLine)) {
-            command = trimmedLine;
-            break; // Found a line that looks like a command
-        }
+async function generateCommandGemini(
+  userPrompt,
+  osInfo,
+  shellInfo,
+  apiKey,
+  explainMode
+) {
+  const config = {
+    responseMimeType: 'text/plain',
+  };
+  const model = 'gemini-2.0-flash-lite';
 
-        // If no specific command line found and not in explain mode code block,
-        // the first non-empty non-conversational line might be the command or part of it.
-        // Keep this as a fallback if the above doesn't match, but prioritize the above.
-         if (!commandStartRegex.test(command) && !conversationalLineRegex.test(trimmedLine) && !explainMode) {
-             command = trimmedLine; // Use the first non-conversational line as a potential command start
-             // Don't break yet, a later line might be a better match with commandStartRegex
-         }
-    }
+  const explainText = explainMode
+    ? 'First, provide a brief explanation of the command. Then, provide the command, ideally in a fenced code block (e.g., ```bash\n...\n```).\n'
+    : 'Respond only with the command, ideally in a fenced code block (e.g., ```bash\n...\n```). No commentary or headings.\n';
 
-     // Basic cleanup: remove leading/trailing quotes or backticks
-    command = command.replace(/^['"`\s]+/, '').replace(/['"`\s]+$/, ''); // Also trim whitespace
+  const fullPrompt = `
+  You are a helpful shell (${shellInfo}) assistant, running on ${osInfo} OS.
+  The user will ask for a task he wants to accomplish or needs help with.
+  For example: "List all files in this folder", "Remove all docker containers", "List files in current directory and save to file.txt", etc.
+  Your goal is to provide the user with a safe and correct shell command(s) to accomplish the task.
+  ${explainText}
+  Task: "${userPrompt}"
+  `.trim();
 
-     // Basic check to prevent just returning conversational filler if extraction failed
-    if (conversationalLineRegex.test(command.toLowerCase()) || command.split(/\s+/).length < 1) {
-        console.warn('Warning: Could not confidently extract a command from the model output. Reviewing full output.');
-         // In case of extraction failure, return the full generated text for user review
-         return generatedText;
-    }
+  try {
+    const response = await fetch(
+      `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
+      {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+          'x-goog-api-key': apiKey,
+        },
+        body: JSON.stringify({
+          contents: [
+            {
+              parts: [
+                {
+                  text: fullPrompt,
+                },
+              ],
+              role: 'user',
+            },
+          ],
+          generationConfig: config,
+        }),
+      }
+    ).then(res => res.json());
 
+    const rawOutput = response.candidates[0].content.parts[0].text;
 
-    return command;
+    if (!rawOutput || rawOutput.trim() === '') {
+      throw new Error('Gemini API returned an empty response.');
+    }
 
+    return rawOutput.trim();
   } catch (error) {
-    console.error('Error generating command with local model:', error);
-    throw new Error('Local model inference failed.');
+    console.error('Google Gemini API error:', error.message);
+    let detail = '';
+    if (error.status) detail += ` Status: ${error.status}`;
+    if (error.code) detail += ` Code: ${error.code}`;
+    if (error.details) detail += ` Details: ${JSON.stringify(error.details)}`;
+    if (error.stack) console.error('Stack:', error.stack);
+
+    throw new Error(`Google Gemini API request failed.${detail}`);
   }
 }
 
-
 // Function to generate command using OpenAI API
-async function generateCommandOpenAI(userPrompt, osInfo, apiKey, explainMode) {
-   const explainText = explainMode
-    ? 'Explain what the command does, then return it.\n'
-    : '';
+async function generateCommandOpenAI(
+  userPrompt,
+  osInfo,
+  shellInfo,
+  apiKey,
+  explainMode
+) {
+  const explainText = explainMode
+    ? 'First, provide a brief explanation of the command. Then, provide the command, ideally in a fenced code block (e.g., ```bash\n...\n```).\n'
+    : 'Respond only with the command, ideally in a fenced code block (e.g., ```bash\n...\n```). No commentary or headings.\n'; // Added instruction for code block
 
   const fullPrompt = `
-You are a shell assistant.
-OS: ${osInfo}
-${explainText}Respond only with safe and correct shell command(s), no commentary or headings.
+You are a helpful shell (${shellInfo}) assistant, running on ${osInfo} OS.
+  The user will ask for a task he wants to accomplish or needs help with.
+  For example: "List all files in this folder", "Remove all docker containers", "List files in current directory and save to file.txt", etc.
+  Your goal is to provide the user with a safe and correct shell command(s) to accomplish the task.
+${explainText}
 Task: "${userPrompt}"
 `.trim();
 
+  try {
+    const response = await fetch('https://api.openai.com/v1/chat/completions', {
+      method: 'POST',
+      headers: {
+        Authorization: `Bearer ${apiKey}`,
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        model: 'gpt-3.5-turbo-0125',
+        messages: [{ role: 'user', content: fullPrompt }],
+        temperature: 0.3,
+      }),
+    });
+
+    if (!response.ok) {
+      const errorBody = await response.json(); // Attempt to parse error body
+      console.error('OpenAI API error:', errorBody);
+      let errorMessage = `OpenAI API request failed: ${response.status} ${response.statusText}`;
+      if (errorBody && errorBody.error && errorBody.error.message) {
+        errorMessage += ` - ${errorBody.error.message}`;
+      }
+      throw new Error(errorMessage);
+    }
 
-  const response = await fetch('https://api.openai.com/v1/chat/completions', {
-    method: 'POST',
-    headers: {
-      Authorization: `Bearer ${apiKey}`,
-      'Content-Type': 'application/json',
-    },
-    body: JSON.stringify({
-      model: 'gpt-3.5-turbo-0125',
-      messages: [{ role: 'user', content: fullPrompt }],
-      temperature: 0.3,
-    }),
-  });
+    const data = await response.json();
+    const rawOutput = data.choices[0].message.content.trim();
 
-  if (!response.ok) {
-    console.error('OpenAI API error:', await response.text());
-    throw new Error('OpenAI API request failed.');
+    return rawOutput;
+  } catch (error) {
+    // Handle network errors etc.
+    console.error('OpenAI API communication error:', error.message);
+    throw new Error(`Failed to communicate with OpenAI API: ${error.message}`);
   }
-
-  const data = await response.json();
-  const output = data.choices[0].message.content.trim();
-  return output;
 }
 
-
 async function main() {
+  // Ensure readline is closed on process exit for clean shutdown
+  process.on('exit', () => {
+    if (!rl.closed) {
+      rl.close();
+    }
+  });
+
+  // Handle Ctrl+C (SIGINT) gracefully
+  rl.on('SIGINT', () => {
+    console.log('\nOperation cancelled.');
+    if (!rl.closed) {
+      rl.close();
+    }
+    process.exit(1);
+  });
+
   const args = process.argv.slice(2);
 
   let config = {
-      provider: 'local'
+    provider: 'local',
   };
   if (fs.existsSync(CONFIG_PATH)) {
-      try {
-          const existingConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
-           // Merge existing config with defaults, ensuring 'provider' is kept if present
-          config = { ...config, ...existingConfig };
-      } catch (e) {
-          console.error('Error reading config file:', e.message);
-          // Keep default config if file is corrupt
-      }
+    try {
+      const existingConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
+      // Merge existing config with defaults, ensuring 'provider' is kept if present
+      config = { ...config, ...existingConfig };
+    } catch (e) {
+      console.error('Error reading config file:', e.message);
+      // Keep default config if file is corrupt
+    }
   }
 
+  // Ensure provider is one of the valid options
+  if (!['local', 'openai', 'gemini'].includes(config.provider)) {
+    console.warn(
+      `Invalid provider "${config.provider}" found in config. Falling back to "local". Run "ai config" to fix.`
+    );
+    config.provider = 'local';
+  }
 
   if (args[0] === 'config') {
     console.log('\nConfigure cmd-ai settings.');
@@ -359,24 +678,32 @@ async function main() {
     const currentProvider = config.provider || 'local';
     console.log(`Current provider: ${currentProvider}`);
 
-    const provider = await ask('Choose AI provider (local, openai) [local]: ');
+    const provider = await ask(
+      'Choose AI provider (local, openai, gemini) [local]: '
+    );
     const selectedProvider = provider.trim().toLowerCase() || 'local';
 
-    if (selectedProvider !== 'local' && selectedProvider !== 'openai') {
-        console.error('Invalid provider selected. Please choose "local" or "openai".');
-        rl.close();
-        process.exit(1);
+    if (!['local', 'openai', 'gemini'].includes(selectedProvider)) {
+      console.error(
+        'Invalid provider selected. Please choose "local", "openai", or "gemini".'
+      );
+      rl.close();
+      process.exit(1);
     }
 
     config.provider = selectedProvider;
 
     if (config.provider === 'openai') {
+      // Clear Gemini key if switching from gemini
+      if (config.geminiApiKey) delete config.geminiApiKey;
       console.log('\nTo use the OpenAI provider, you need a valid API key.');
       console.log('If you don’t have one, follow these steps:\n');
       console.log('1. Go to https://platform.openai.com/account/api-keys');
       console.log('2. Log in or create a free OpenAI account');
       console.log('3. Click “+ Create new secret key”');
-      console.log('4. Copy the key (starts with "sk-...") and paste it below\n');
+      console.log(
+        '4. Copy the key (starts with "sk-...") and paste it below\n'
+      );
 
       const key = await ask('Paste your OpenAI API key: ');
       const trimmed = key.trim();
@@ -388,32 +715,68 @@ async function main() {
         rl.close();
         process.exit(1);
       }
-      config.apiKey = trimmed;
     } else {
-        // Remove apiKey if switching away from openai
-        if (config.apiKey) {
-            delete config.apiKey;
-        }
-        // Trigger download if local is selected
-        try {
-             await downloadLocalModel();
-        } catch (e) {
-            console.error(`\nFailed to download local model. You may need to try running "ai config" again or check your connection.`);
-             rl.close();
-             process.exit(1);
-        }
+      if (config.apiKey) delete config.apiKey;
+    }
+
+    if (config.provider === 'gemini') {
+      // Remove apiKey if switching away from openai
+      if (config.apiKey) delete config.apiKey;
+      console.log(
+        '\nTo use the Google Gemini provider, you need a valid API key.'
+      );
+      console.log('If you don’t have one, follow these steps:\n');
+      console.log('1. Go to https://aistudio.google.com/app/apikey');
+      console.log('2. Log in or create a Google account');
+      console.log('3. Create or copy an existing API key\n');
+
+      const key = await ask('Paste your Google Gemini API key: ');
+      const trimmed = key.trim();
+
+      if (trimmed.length < 30) {
+        // Basic length check
+        console.error(
+          'Invalid key format. The key should be longer than 30 characters.'
+        );
+        rl.close();
+        process.exit(1);
+      }
+      config.geminiApiKey = trimmed;
+    } else {
+      // Remove geminiApiKey if switching away from gemini
+      if (config.geminiApiKey) delete config.geminiApiKey;
     }
 
-    fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
-    console.log('\n✅ Configuration saved successfully.');
     if (config.provider === 'local') {
-         console.log(`Using local model "${LOCAL_MODEL_ID}".`);
-         console.log('The model files are now downloaded or updated.');
-    } else if (config.provider === 'openai') {
-         console.log('Using OpenAI API.');
+      // Trigger download/check if local is selected
+      try {
+        await downloadLocalModel();
+        localPipeline = null; // Ensure pipeline is null to force load on first generation
+      } catch (e) {
+        console.error(`\nFailed during local model setup: ${e.message}`);
+        rl.close();
+        process.exit(1);
+      }
+    }
+
+    try {
+      fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
+      console.log('\n✅ Configuration saved successfully.');
+      if (config.provider === 'local') {
+        console.log(`Using local model "${LOCAL_MODEL_ID}".`);
+        console.log('Model files are downloaded or updated.');
+      } else if (config.provider === 'openai') {
+        console.log('Using OpenAI API.');
+      } else if (config.provider === 'gemini') {
+        console.log('Using Google Gemini API.');
+      }
+      console.log('You can now run commands like:');
+      console.log('  ai list all files in this folder\n');
+    } catch (e) {
+      console.error('Error saving config file:', e.message);
+      process.exit(1);
     }
-    console.log('You can now run commands like:');
-    console.log('  ai list all files in this folder\n');
+
     rl.close();
     return;
   }
@@ -428,11 +791,16 @@ async function main() {
     const __filename = fileURLToPath(import.meta.url);
     const __dirname = path.dirname(__filename);
 
-    const pkg = JSON.parse(
-      fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8')
-    );
+    try {
+      const pkg = JSON.parse(
+        fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8')
+      );
+      console.log(`cmd-ai v${pkg.version}`);
+    } catch (e) {
+      console.error('Could not read package.json to determine version.');
+      console.log('cmd-ai version unknown');
+    }
 
-    console.log(`cmd-ai v${pkg.version}`);
     rl.close();
     return;
   }
@@ -449,14 +817,27 @@ async function main() {
       rl.close();
       return;
     }
-    const history = JSON.parse(fs.readFileSync(HISTORY_PATH, 'utf-8'));
-    history.forEach((entry, idx) => {
-      console.log(
-        `\n#${idx + 1} (${entry.timestamp})\nPrompt: ${
-          entry.prompt
-        }\nCommand:\n${entry.command}\nExecuted: ${entry.executed}${entry.provider ? ` (Provider: ${entry.provider})` : ''}`
-      );
-    });
+    try {
+      const history = JSON.parse(fs.readFileSync(HISTORY_PATH, 'utf-8'));
+      if (history.length === 0) {
+        console.log('Command history is empty.');
+        rl.close();
+        return;
+      }
+      history.forEach((entry, idx) => {
+        console.log(
+          `\n--- History Entry #${idx + 1} ---\nTimestamp: ${
+            entry.timestamp
+          }\nPrompt: ${entry.prompt}\nCommand:\n${entry.command}\nExecuted: ${
+            entry.executed
+          }${entry.provider ? ` (Provider: ${entry.provider})` : ''}${
+            entry.notes ? `\nNotes: ${entry.notes}` : ''
+          }`
+        );
+      });
+    } catch (e) {
+      console.error('Error reading or parsing history file:', e.message);
+    }
     rl.close();
     return;
   }
@@ -468,70 +849,158 @@ async function main() {
     arg => !['--explain', '--dry'].includes(arg)
   );
   const userPrompt = filteredArgs.join(' ');
-  const osInfo = `${os.platform()} ${os.release()}`;
+  const osInfo = `${os.platform()} ${os.release()} (${os.arch()})`; // Add architecture
+  const shellInfo = process.env.SHELL
+    ? path.basename(process.env.SHELL)
+    : 'sh, zsh, ksh, etc';
 
   if (!userPrompt) {
-       printHelp();
-       rl.close();
-       process.exit(0);
+    printHelp();
+    rl.close();
+    process.exit(0);
   }
 
-  let output = '';
-  let executedProvider = config.provider; // Track which provider was actually used
+  let rawModelOutput = '';
+  let executedProvider = config.provider;
 
   try {
     if (config.provider === 'local') {
-        output = await generateCommandLocal(userPrompt, osInfo, explainMode);
+      console.log('Explain mode:', explainMode);
+      rawModelOutput = await generateCommandLocal(
+        userPrompt,
+        osInfo,
+        shellInfo,
+        explainMode
+      );
     } else if (config.provider === 'openai') {
-        if (!config.apiKey) {
-            console.error('Missing OpenAI API key for the "openai" provider. Please run "ai config" first.');
-             rl.close();
-            process.exit(1);
-        }
-        output = await generateCommandOpenAI(userPrompt, osInfo, config.apiKey, explainMode);
-    } else {
-        // Should not happen with default config handling, but as a safeguard
-        console.error(`Invalid provider configured: ${config.provider}. Please run "ai config".`);
+      if (!config.apiKey) {
+        console.error(
+          'Missing OpenAI API key for the "openai" provider. Please run "ai config" first.'
+        );
+        rl.close();
+        process.exit(1);
+      }
+      rawModelOutput = await generateCommandOpenAI(
+        userPrompt,
+        osInfo,
+        shellInfo,
+        config.apiKey,
+        explainMode
+      );
+    } else if (config.provider === 'gemini') {
+      if (!config.geminiApiKey) {
+        console.error(
+          'Missing Google Gemini API key for the "gemini" provider. Please run "ai config" first.'
+        );
         rl.close();
         process.exit(1);
+      }
+      // Gemini API does not support streaming by default with generateContent, so no progress display here easily
+      rawModelOutput = await generateCommandGemini(
+        userPrompt,
+        osInfo,
+        shellInfo,
+        config.geminiApiKey,
+        explainMode
+      );
+    } else {
+      // Should not happen with default config handling, but as a safeguard
+      console.error(
+        `Invalid provider configured: ${config.provider}. Please run "ai config".`
+      );
     }
   } catch (error) {
-      console.error(`\nError generating command: ${error.message}`); // Add newline before error
-      rl.close();
-      process.exit(1);
+    console.error(`\nError generating command: ${error.message}`);
+    saveHistory({
+      prompt: userPrompt,
+      command: 'Error generating command',
+      executed: false,
+      provider: executedProvider,
+      notes: `Generation failed: ${error.message}`,
+    });
+    rl.close();
+    process.exit(1);
   }
 
+  // --- Parse the raw model output ---
+  const { explanation, command } = parseModelOutput(
+    rawModelOutput,
+    explainMode
+  );
+
+  console.log(`\nAI Response (Provider: ${executedProvider}):`);
+
+  // Print explanation if explain mode is on and explanation was extracted
+  if (explainMode && explanation) {
+    console.log('\n--- Explanation ---');
+    console.log(explanation);
+  }
 
-  console.log(`\nAI Response (Provider: ${executedProvider}):\n`);
-  console.log(output);
+  // Always print the extracted command
+  console.log('\n--- Proposed Command ---');
+  console.log(command);
+  console.log('----------------------');
+
+  if (!command || command.trim() === '') {
+    console.error('\nCould not extract a valid command from the AI response.');
+    console.log('Full AI output was:');
+    console.log(rawModelOutput); // Show user the raw output if parsing failed
+    saveHistory({
+      prompt: userPrompt,
+      command: rawModelOutput,
+      executed: false,
+      provider: executedProvider,
+      notes: 'Command extraction failed',
+    }); // Save raw output if command extraction fails
+    rl.close();
+    return;
+  }
 
-  if (isDangerous(output)) {
+  if (isDangerous(command)) {
     console.error(
       '\n** WARNING: This command looks dangerous and will not be executed automatically. **'
     );
-    saveHistory({ prompt: userPrompt, command: output, executed: false, provider: executedProvider });
+    saveHistory({
+      prompt: userPrompt,
+      command: command,
+      executed: false,
+      provider: executedProvider,
+      notes: 'Dangerous command detected',
+    });
     rl.close();
     return;
   }
 
   const confirm = await ask(
     dryRun
-      ? '\n[Dry run] Press ENTER to simulate, or Ctrl+C to cancel: '
-      : '\nDo you want to run it? (Y/n): '
+      ? '\n[Dry run] Press ENTER to simulate execution, or Ctrl+C to cancel: '
+      : '\nDo you want to run the proposed command(s)? (Y/n): ' // Clearer prompt for potential multiple commands
   );
   const shouldRun =
     confirm.trim() === '' || confirm.trim().toLowerCase() === 'y';
 
   if (!shouldRun) {
     console.log('Operation cancelled.');
-    saveHistory({ prompt: userPrompt, command: output, executed: false, provider: executedProvider });
+    saveHistory({
+      prompt: userPrompt,
+      command: command,
+      executed: false,
+      provider: executedProvider,
+      notes: 'Cancelled by user',
+    });
     rl.close();
     return;
   }
 
   if (dryRun) {
     console.log('\n[Dry run] Command not executed.');
-    saveHistory({ prompt: userPrompt, command: output, executed: false, provider: executedProvider });
+    saveHistory({
+      prompt: userPrompt,
+      command: command,
+      executed: false,
+      provider: executedProvider,
+      notes: 'Dry run',
+    });
     rl.close();
     return;
   }
@@ -539,16 +1008,56 @@ async function main() {
   // Close readline before executing command to avoid interference
   rl.close();
 
-  exec(output, (error, stdout, stderr) => {
+  console.log('\nExecuting command...');
+  exec(command, (error, stdout, stderr) => {
     if (error) {
-      console.error(`Execution error:\n${error.message}`);
+      console.error(`\nExecution error:\n${error.message}`);
+      saveHistory({
+        prompt: userPrompt,
+        command: command,
+        executed: false,
+        provider: executedProvider,
+        notes: `Execution failed: ${error.message}`,
+      });
     } else {
-      if (stderr) console.error(`Stderr:\n${stderr}`);
-      console.log(`Output:\n${stdout}`);
+      if (stdout) console.log(`\nStdout:\n${stdout}`);
+      if (stderr) console.error(`\nStderr:\n${stderr}`);
+      if (!stdout && !stderr) {
+        console.log('\nCommand executed successfully with no output.');
+      }
+      saveHistory({
+        prompt: userPrompt,
+        command: command,
+        executed: true,
+        provider: executedProvider,
+      });
     }
-    saveHistory({ prompt: userPrompt, command: output, executed: true, provider: executedProvider });
-    rl.close(); // Close readline interface after execution finishes
   });
 }
 
+// Handle unhandled promise rejections
+process.on('unhandledRejection', (reason, promise) => {
+  console.error('Unhandled Rejection:', reason);
+  // Application specific error handling
+  if (rl && !rl.closed) {
+    rl.close();
+  }
+  if (!process.exitCode) {
+    // Prevent double exit
+    process.exit(1); // Exit with a non-zero code
+  }
+});
+
+// Handle uncaught exceptions
+process.on('uncaughtException', err => {
+  console.error('Uncaught Exception:', err);
+  if (rl && !rl.closed) {
+    rl.close();
+  }
+  if (!process.exitCode) {
+    // Prevent double exit
+    process.exit(1); // Exit with a non-zero code
+  }
+});
+
 main();
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index 40ac5b5..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-  "name": "cmd-ai",
-  "version": "1.0.2",
-  "lockfileVersion": 3,
-  "requires": true,
-  "packages": {
-    "": {
-      "name": "cmd-ai",
-      "version": "1.0.2",
-      "license": "MIT",
-      "bin": {
-        "ai": "bin/ai.js"
-      },
-      "engines": {
-        "node": ">=18"
-      }
-    }
-  }
-}


Reviews:
user: login: BrodaNoel
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T19:00:37Z
user: login: BrodaNoel
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T19:35:19Z
user: login: BrodaNoel
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T19:36:56Z
user: login: BrodaNoel
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T19:38:08Z
user: login: cacuscacus
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-10T16:52:05Z
user: login: cacuscacus
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-10T16:52:27Z
user: login: cacuscacus
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-10T16:53:20Z
user: login: cacuscacus
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-10T16:56:53Z
user: login: cacuscacus
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-10T16:57:20Z
user: login: BrodaNoel
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-10T18:06:13Z
user: login: BrodaNoel
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-10T18:06:43Z
user: login: BrodaNoel
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-10T18:07:26Z
user: login: BrodaNoel
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-10T19:16:43Z
user: login: BrodaNoel
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-12T22:18:39Z
user: login: BrodaNoel
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-12T22:19:21Z
user: login: BrodaNoel
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-12T22:31:52Z
user: login: cacuscacus
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T02:10:50Z
user: login: BrodaNoel
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-13T02:11:58Z
user: login: BrodaNoel
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-13T02:12:38Z

Review comments:
diff_hunk: @@ -1 +1,3 @@
-/node_modules
\ No newline at end of file
+/node_modules
+.vscode/settings.json
+package-lock.json
path: .gitignore
user: login: BrodaNoel
body: Remove the `package-lock.json`. It should be pushed, always.
created_at: 2025-05-09T19:00:37Z
author_association: OWNER
diff_hunk: @@ -1,7 +0,0 @@
-{
path: .vscode/settings.json
user: login: BrodaNoel
body: Keeps this file.
This file is `vscode` but it's already an estandar for defining basic configurations (most of new IDE are forks of vscode, so they understand it)
created_at: 2025-05-09T19:35:19Z
author_association: OWNER
diff_hunk: @@ -7,14 +7,22 @@ import { fileURLToPath } from 'url';
 import readline from 'readline';
 import { exec } from 'child_process';
 
+import { GoogleGenAI } from '@google/genai';
path: bin/ai.js
user: login: BrodaNoel
body: I would avoid adding these dependencies, and just call the endpoint with the native `fetch` function (as it's made with OpenAI).
These dependencies increase heavily the whole "build" with no sense, because we just want to do a `fetch`.
created_at: 2025-05-09T19:36:56Z
author_association: OWNER
diff_hunk: @@ -23,71 +31,80 @@ function saveHistory(entry) {
   let history = [];
   if (fs.existsSync(HISTORY_PATH)) {
     try {
-      history = JSON.parse(fs.readFileSync(HISTORY_PATH, 'utf-8'));
-    } catch {}
+      // Limit history size to prevent huge files
+      history = JSON.parse(fs.readFileSync(HISTORY_PATH, 'utf-8')).slice(-1000); // Keep last 1000 entries
+    } catch (e) {
+      console.error("Error reading history file, starting fresh:", e.message);
+      history = [];
+    }
   }
   history.push({ ...entry, timestamp: new Date().toISOString() });
-  fs.writeFileSync(HISTORY_PATH, JSON.stringify(history, null, 2));
+  try {
+    fs.writeFileSync(HISTORY_PATH, JSON.stringify(history, null, 2));
+  } catch (e) {
+    console.error("Error writing history file:", e.message);
+  }
 }
 
 function isDangerous(command) {
-  const blackListPatterns = [
-    'rm -rf /',
-    'rm -rf ~',
-    'rm -rf .*',
-    'rm -rf *',
-    'rm --no-preserve-root',
-    'rm -r --no-preserve-root /',
-    'mkfs',
-    'mkfs.ext4',
-    'mkfs.xfs',
-    'mkfs.vfat',
-    'dd if=',
-    ':(){:|:&};:',
-    '>:()',
-    'shutdown',
-    'reboot',
-    'halt',
-    'poweroff',
-    'init 0',
-    'init 6',
-    'kill -9 1',
-    'mv /',
-    'chmod 000',
-    'chmod -R 000 /',
-    'chown root',
-    'yes > /dev/sda',
-    'yes > /dev/null',
-    '>/dev/sda',
-    '>/dev/null',
-    'mount -o bind / /dev/null',
-    'crontab -r',
-    'echo .* >',
-    'cat /dev/urandom >',
-    'find / -exec rm',
-    'find / -delete',
-  ];
-
+  // Normalize command for safer checking
   const normalized = command
     .toLowerCase()
     .replace(/\s+/g, ' ') // normalize spacing
-    .replace(/\n/g, ';'); // treat newlines like semicolons
+    .replace(/\\+$/, '') // remove trailing backslashes used for line continuation, check single lines
+    .replace(/\n/g, ';'); // treat newlines like semicolons for splitting
+
+  // Split into potential subcommands (separated by ;, &&, ||)
+  // Basic split might not handle complex quoting/escaping perfectly, but good for common cases
+  const subcommands = normalized.split(/;|&&|\|\||\(|\)|\{|\}/).map(s => s.trim()).filter(s => s !== '');
+
+  const blackListPatterns = [
+    'rm -rf /', 'rm -rf ~', 'rm -rf .*', 'rm -rf *',
path: bin/ai.js
user: login: BrodaNoel
body: This is gonna be modified by `prettier` as soon as I save the file.
created_at: 2025-05-09T19:38:08Z
author_association: OWNER
diff_hunk: @@ -7,14 +7,22 @@ import { fileURLToPath } from 'url';
 import readline from 'readline';
 import { exec } from 'child_process';
 
+import { GoogleGenAI } from '@google/genai';
path: bin/ai.js
user: login: cacuscacus
body: you are right, it is totally useless. the only catch I might see if for some reason in the future google changes the API path it will break this provider... but I guess that is not likely. worth the risk in order to simplify
created_at: 2025-05-10T16:52:05Z
author_association: COLLABORATOR
diff_hunk: @@ -1 +1,3 @@
-/node_modules
\ No newline at end of file
+/node_modules
+.vscode/settings.json
+package-lock.json
path: .gitignore
user: login: cacuscacus
body: done
created_at: 2025-05-10T16:52:27Z
author_association: COLLABORATOR
diff_hunk: @@ -1,7 +0,0 @@
-{
path: .vscode/settings.json
user: login: cacuscacus
body: hmm I'm not so sure about this but you are the chief, I'll add it back
created_at: 2025-05-10T16:53:20Z
author_association: COLLABORATOR
diff_hunk: @@ -1,7 +0,0 @@
-{
path: .vscode/settings.json
user: login: cacuscacus
body: the explanation:
you are forcing each user that wants to collaborate to use a thirdparty tool, an extra tool (prettier), which might not be optimal for everyone, most people might not even use vscode.
is it really necessary? it is the same as a dependency, why not keep the standard and avoid getting way to custom in the code styling topic?

but this is just my opinion, I'll recover the file and leave it as it was.
up to you in the future if you think my argument is good or not.
created_at: 2025-05-10T16:56:53Z
author_association: COLLABORATOR
diff_hunk: @@ -23,71 +31,80 @@ function saveHistory(entry) {
   let history = [];
   if (fs.existsSync(HISTORY_PATH)) {
     try {
-      history = JSON.parse(fs.readFileSync(HISTORY_PATH, 'utf-8'));
-    } catch {}
+      // Limit history size to prevent huge files
+      history = JSON.parse(fs.readFileSync(HISTORY_PATH, 'utf-8')).slice(-1000); // Keep last 1000 entries
+    } catch (e) {
+      console.error("Error reading history file, starting fresh:", e.message);
+      history = [];
+    }
   }
   history.push({ ...entry, timestamp: new Date().toISOString() });
-  fs.writeFileSync(HISTORY_PATH, JSON.stringify(history, null, 2));
+  try {
+    fs.writeFileSync(HISTORY_PATH, JSON.stringify(history, null, 2));
+  } catch (e) {
+    console.error("Error writing history file:", e.message);
+  }
 }
 
 function isDangerous(command) {
-  const blackListPatterns = [
-    'rm -rf /',
-    'rm -rf ~',
-    'rm -rf .*',
-    'rm -rf *',
-    'rm --no-preserve-root',
-    'rm -r --no-preserve-root /',
-    'mkfs',
-    'mkfs.ext4',
-    'mkfs.xfs',
-    'mkfs.vfat',
-    'dd if=',
-    ':(){:|:&};:',
-    '>:()',
-    'shutdown',
-    'reboot',
-    'halt',
-    'poweroff',
-    'init 0',
-    'init 6',
-    'kill -9 1',
-    'mv /',
-    'chmod 000',
-    'chmod -R 000 /',
-    'chown root',
-    'yes > /dev/sda',
-    'yes > /dev/null',
-    '>/dev/sda',
-    '>/dev/null',
-    'mount -o bind / /dev/null',
-    'crontab -r',
-    'echo .* >',
-    'cat /dev/urandom >',
-    'find / -exec rm',
-    'find / -delete',
-  ];
-
+  // Normalize command for safer checking
   const normalized = command
     .toLowerCase()
     .replace(/\s+/g, ' ') // normalize spacing
-    .replace(/\n/g, ';'); // treat newlines like semicolons
+    .replace(/\\+$/, '') // remove trailing backslashes used for line continuation, check single lines
+    .replace(/\n/g, ';'); // treat newlines like semicolons for splitting
+
+  // Split into potential subcommands (separated by ;, &&, ||)
+  // Basic split might not handle complex quoting/escaping perfectly, but good for common cases
+  const subcommands = normalized.split(/;|&&|\|\||\(|\)|\{|\}/).map(s => s.trim()).filter(s => s !== '');
+
+  const blackListPatterns = [
+    'rm -rf /', 'rm -rf ~', 'rm -rf .*', 'rm -rf *',
path: bin/ai.js
user: login: cacuscacus
body: ok, I'll do. but check the previous comment just FYI
created_at: 2025-05-10T16:57:20Z
author_association: COLLABORATOR
diff_hunk: @@ -1,7 +0,0 @@
-{
path: .vscode/settings.json
user: login: BrodaNoel
body: prettier just force an standart to be implemented.
created_at: 2025-05-10T18:06:13Z
author_association: OWNER
diff_hunk: @@ -1,7 +1,7 @@
 {
-  "editor.formatOnSave": true,
-  "[javascript]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
-  },
-  "prettier.configPath": ".prettierrc"
+    "editor.formatOnSave": true,
+    "[javascript]": {
+        "editor.defaultFormatter": "esbenp.prettier-vscode"
+    },
+    "prettier.configPath": ".prettierrc"
path: .vscode/settings.json
user: login: BrodaNoel
body: I'll have to add the config for JSON files, because your shitty computer keeps adding 4 spaces.
created_at: 2025-05-10T18:06:43Z
author_association: OWNER
diff_hunk: @@ -1 +1,3 @@
-/node_modules
\ No newline at end of file
+/node_modules
+.vscode/settings.json
+package-lock.json
path: .gitignore
user: login: BrodaNoel
body: Push your changes. I still see the `package-lock.json` added there
created_at: 2025-05-10T18:07:26Z
author_association: OWNER
diff_hunk: @@ -1,7 +1,7 @@
 {
-  "editor.formatOnSave": true,
-  "[javascript]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
-  },
-  "prettier.configPath": ".prettierrc"
+    "editor.formatOnSave": true,
+    "[javascript]": {
+        "editor.defaultFormatter": "esbenp.prettier-vscode"
+    },
+    "prettier.configPath": ".prettierrc"
path: .vscode/settings.json
user: login: BrodaNoel
body: I already added it on another PR. So, don't fix it here.
created_at: 2025-05-10T19:16:43Z
author_association: OWNER
diff_hunk: @@ -1 +1,2 @@
-/node_modules
\ No newline at end of file
+/node_modules
+package-lock.json
path: .gitignore
user: login: BrodaNoel
body: Remove `package-lock.json` from there! That file is required to be pushed.
created_at: 2025-05-12T22:18:39Z
author_association: OWNER
diff_hunk: @@ -1,19 +0,0 @@
-{
path: package-lock.json
user: login: BrodaNoel
body: Get this file back.
created_at: 2025-05-12T22:19:21Z
author_association: OWNER
diff_hunk: @@ -1 +1,2 @@
-/node_modules
\ No newline at end of file
+/node_modules
+package-lock.json
path: .gitignore
user: login: cacuscacus
body: package-lock? wtf? why? that is environment dependent or am I missing something??
created_at: 2025-05-13T02:10:50Z
author_association: COLLABORATOR
diff_hunk: @@ -1 +1,2 @@
-/node_modules
\ No newline at end of file
+/node_modules
+package-lock.json
path: .gitignore
user: login: BrodaNoel
body: You are missing something.
created_at: 2025-05-13T02:11:58Z
author_association: OWNER
diff_hunk: @@ -1 +1,2 @@
-/node_modules
\ No newline at end of file
+/node_modules
+package-lock.json
path: .gitignore
user: login: BrodaNoel
body: Don't worry. I already added it
created_at: 2025-05-13T02:12:38Z
author_association: OWNER

PR comments:
user: login: BrodaNoel
created_at: 2025-05-12T03:00:23Z
body: @cacuscacus update this so I can take a look
author_association: OWNER
user: login: cacuscacus
created_at: 2025-05-12T17:21:44Z
body: > @cacuscacus update this so I can take a look

should be ok now, take a look
author_association: COLLABORATOR
user: login: BrodaNoel
created_at: 2025-05-12T22:32:20Z
body: Next time keep the PR 3 million times smaller.
author_association: OWNER