diff --git a/README.md b/README.md index 60f9d623..3dcfc25a 100644 --- a/README.md +++ b/README.md @@ -296,15 +296,19 @@ Options: ### Custom Instructions -You can provide custom instructions to tailor Grok's behavior to your project by creating a `.grok/GROK.md` file in your project directory: +You can provide custom instructions to tailor Grok's behavior to your project or globally. Grok CLI supports both project-level and global custom instructions. + +#### Project-Level Instructions + +Create a `.grok/GROK.md` file in your project directory to provide instructions specific to that project: ```bash mkdir .grok ``` -Create `.grok/GROK.md` with your custom instructions: +Create `.grok/GROK.md` with your project-specific instructions: ```markdown -# Custom Instructions for Grok CLI +# Custom Instructions for This Project Always use TypeScript for any new code files. When creating React components, use functional components with hooks. @@ -313,7 +317,33 @@ Always add JSDoc comments for public functions and interfaces. Follow the existing code style and patterns in this project. ``` -Grok will automatically load and follow these instructions when working in your project directory. The custom instructions are added to Grok's system prompt and take priority over default behavior. +#### Global Instructions + +For instructions that apply across all projects, create `~/.grok/GROK.md` in your home directory: + +```bash +mkdir -p ~/.grok +``` + +Create `~/.grok/GROK.md` with your global instructions: +```markdown +# Global Custom Instructions for Grok CLI + +Always prioritize code readability and maintainability. +Use descriptive variable names and add comments for complex logic. +Follow best practices for the programming language being used. +When suggesting code changes, consider performance implications. +``` + +#### Priority Order + +Grok will load custom instructions in the following priority order: +1. **Project-level** (`.grok/GROK.md` in current directory) - takes highest priority +2. **Global** (`~/.grok/GROK.md` in home directory) - fallback if no project instructions exist + +If both files exist, project instructions will be used. If neither exists, Grok operates with its default behavior. + +The custom instructions are added to Grok's system prompt and influence its responses across all interactions in the respective context. ## Morph Fast Apply (Optional) diff --git a/package.json b/package.json index 46f3d744..8b516a2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-kit/grok-cli", - "version": "0.0.33", + "version": "0.0.34", "description": "An open-source AI agent that brings the power of Grok directly into your terminal.", "type": "module", "main": "dist/index.js", diff --git a/src/index.ts b/src/index.ts index c88ec3df..8d3dc11c 100755 --- a/src/index.ts +++ b/src/index.ts @@ -354,7 +354,7 @@ program if (!apiKey) { console.error( - "❌ Error: API key required. Set GROK_API_KEY environment variable, use --api-key flag, or save to ~/.grok/user-settings.json" + "❌ Error: API key required. Set GROK_API_KEY environment variable, use --api-key flag, or set \"apiKey\" field in ~/.grok/user-settings.json" ); process.exit(1); } diff --git a/src/tools/morph-editor.ts b/src/tools/morph-editor.ts index e6585e60..561a1b27 100644 --- a/src/tools/morph-editor.ts +++ b/src/tools/morph-editor.ts @@ -1,4 +1,4 @@ -import * as fs from "fs-extra"; +import fs from "fs-extra"; import * as path from "path"; import axios from "axios"; import { ToolResult } from "../types/index.js"; @@ -390,4 +390,4 @@ export class MorphEditorTool { getApiKey(): string { return this.morphApiKey; } -} \ No newline at end of file +} diff --git a/src/tools/search.ts b/src/tools/search.ts index a125faca..3303fbf9 100644 --- a/src/tools/search.ts +++ b/src/tools/search.ts @@ -1,7 +1,7 @@ import { spawn } from "child_process"; import { ToolResult } from "../types/index.js"; import { ConfirmationService } from "../utils/confirmation-service.js"; -import * as fs from "fs-extra"; +import fs from "fs-extra"; import * as path from "path"; export interface SearchResult { diff --git a/src/tools/text-editor.ts b/src/tools/text-editor.ts index 409f4d8a..02e101fc 100644 --- a/src/tools/text-editor.ts +++ b/src/tools/text-editor.ts @@ -1,4 +1,4 @@ -import * as fs from "fs-extra"; +import fs from "fs-extra"; import * as path from "path"; import { writeFile as writeFilePromise } from "fs/promises"; import { ToolResult, EditorCommand } from "../types/index.js"; @@ -468,84 +468,120 @@ export class TextEditorTool { const tokens = str.match(/\b(function|console\.log|return|if|else|for|while)\b/g) || []; return tokens; }; - + const searchTokens = extractTokens(search); const actualTokens = extractTokens(actual); - + if (searchTokens.length !== actualTokens.length) return false; - + for (let i = 0; i < searchTokens.length; i++) { if (searchTokens[i] !== actualTokens[i]) return false; } - + return true; } - private generateDiff( + /** + * Compute Longest Common Subsequence using dynamic programming + * Returns array of indices in oldLines that are part of LCS + */ + private computeLCS(oldLines: string[], newLines: string[]): number[][] { + const m = oldLines.length; + const n = newLines.length; + const dp: number[][] = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0)); + + // Build LCS length table + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + if (oldLines[i - 1] === newLines[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + return dp; + } + + /** + * Extract changes from LCS table + * Returns array of change regions + */ + private extractChanges( oldLines: string[], newLines: string[], - filePath: string - ): string { - const CONTEXT_LINES = 3; - + lcs: number[][] + ): Array<{ oldStart: number; oldEnd: number; newStart: number; newEnd: number }> { const changes: Array<{ oldStart: number; oldEnd: number; newStart: number; newEnd: number; }> = []; - - let i = 0, j = 0; - - while (i < oldLines.length || j < newLines.length) { - while (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) { - i++; - j++; - } - - if (i < oldLines.length || j < newLines.length) { - const changeStart = { old: i, new: j }; - - let oldEnd = i; - let newEnd = j; - - while (oldEnd < oldLines.length || newEnd < newLines.length) { - let matchFound = false; - let matchLength = 0; - - for (let k = 0; k < Math.min(2, oldLines.length - oldEnd, newLines.length - newEnd); k++) { - if (oldEnd + k < oldLines.length && - newEnd + k < newLines.length && - oldLines[oldEnd + k] === newLines[newEnd + k]) { - matchLength++; - } else { - break; - } - } - - if (matchLength >= 2 || (oldEnd >= oldLines.length && newEnd >= newLines.length)) { - matchFound = true; - } - - if (matchFound) { - break; - } - - if (oldEnd < oldLines.length) oldEnd++; - if (newEnd < newLines.length) newEnd++; + + let i = oldLines.length; + let j = newLines.length; + let oldEnd = i; + let newEnd = j; + let inChange = false; + + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) { + // Lines match - if we were in a change, close it + if (inChange) { + changes.unshift({ + oldStart: i, + oldEnd: oldEnd, + newStart: j, + newEnd: newEnd + }); + inChange = false; } - - changes.push({ - oldStart: changeStart.old, - oldEnd: oldEnd, - newStart: changeStart.new, - newEnd: newEnd - }); - - i = oldEnd; - j = newEnd; + i--; + j--; + } else if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) { + // Insertion in new file + if (!inChange) { + oldEnd = i; + newEnd = j; + inChange = true; + } + j--; + } else if (i > 0) { + // Deletion from old file + if (!inChange) { + oldEnd = i; + newEnd = j; + inChange = true; + } + i--; } } + + // Close any remaining change + if (inChange) { + changes.unshift({ + oldStart: 0, + oldEnd: oldEnd, + newStart: 0, + newEnd: newEnd + }); + } + + return changes; + } + + private generateDiff( + oldLines: string[], + newLines: string[], + filePath: string + ): string { + const CONTEXT_LINES = 3; + + // Use LCS-based diff algorithm to find actual changes + const lcs = this.computeLCS(oldLines, newLines); + const changes = this.extractChanges(oldLines, newLines, lcs); const hunks: Array<{ oldStart: number; diff --git a/src/utils/custom-instructions.ts b/src/utils/custom-instructions.ts index f95f081c..e9a87ce5 100644 --- a/src/utils/custom-instructions.ts +++ b/src/utils/custom-instructions.ts @@ -1,18 +1,26 @@ import * as fs from 'fs'; import * as path from 'path'; +import * as os from 'os'; export function loadCustomInstructions(workingDirectory: string = process.cwd()): string | null { try { - const instructionsPath = path.join(workingDirectory, '.grok', 'GROK.md'); + let instructionsPath = path.join(workingDirectory, '.grok', 'GROK.md'); - if (!fs.existsSync(instructionsPath)) { - return null; + if (fs.existsSync(instructionsPath)) { + const customInstructions = fs.readFileSync(instructionsPath, 'utf-8'); + return customInstructions.trim(); } - - const customInstructions = fs.readFileSync(instructionsPath, 'utf-8'); - return customInstructions.trim(); + + instructionsPath = path.join(os.homedir(), '.grok', 'GROK.md'); + + if (fs.existsSync(instructionsPath)) { + const customInstructions = fs.readFileSync(instructionsPath, 'utf-8'); + return customInstructions.trim(); + } + + return null; } catch (error) { console.warn('Failed to load custom instructions:', error); return null; } -} \ No newline at end of file +} diff --git a/src/utils/settings-manager.ts b/src/utils/settings-manager.ts index a2952a1f..76a14fcf 100644 --- a/src/utils/settings-manager.ts +++ b/src/utils/settings-manager.ts @@ -2,6 +2,12 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; +/** + * Current settings version - increment this when adding new models or changing settings structure + * This triggers automatic migration for existing users + */ +const SETTINGS_VERSION = 2; + /** * User-level settings stored in ~/.grok/user-settings.json * These are global settings that apply across all projects @@ -11,6 +17,7 @@ export interface UserSettings { baseURL?: string; // API base URL defaultModel?: string; // User's preferred default model models?: string[]; // Available models list + settingsVersion?: number; // Version for migration tracking } /** @@ -29,10 +36,22 @@ const DEFAULT_USER_SETTINGS: Partial = { baseURL: "https://api.x.ai/v1", defaultModel: "grok-code-fast-1", models: [ - "grok-code-fast-1", + // Grok 4.1 Fast models (2M context, latest - November 2025) + "grok-4-1-fast-reasoning", + "grok-4-1-fast-non-reasoning", + // Grok 4 Fast models (2M context) + "grok-4-fast-reasoning", + "grok-4-fast-non-reasoning", + // Grok 4 flagship (256K context) + "grok-4", "grok-4-latest", + // Grok Code (optimized for coding, 256K context) + "grok-code-fast-1", + // Grok 3 models (131K context) + "grok-3", "grok-3-latest", "grok-3-fast", + "grok-3-mini", "grok-3-mini-fast", ], }; @@ -96,13 +115,22 @@ export class SettingsManager { try { if (!fs.existsSync(this.userSettingsPath)) { // Create default user settings if file doesn't exist - this.saveUserSettings(DEFAULT_USER_SETTINGS); - return { ...DEFAULT_USER_SETTINGS }; + const newSettings = { ...DEFAULT_USER_SETTINGS, settingsVersion: SETTINGS_VERSION }; + this.saveUserSettings(newSettings); + return newSettings; } const content = fs.readFileSync(this.userSettingsPath, "utf-8"); const settings = JSON.parse(content); + // Check if migration is needed + const currentVersion = settings.settingsVersion || 1; + if (currentVersion < SETTINGS_VERSION) { + const migratedSettings = this.migrateSettings(settings, currentVersion); + this.saveUserSettings(migratedSettings); + return migratedSettings; + } + // Merge with defaults to ensure all required fields exist return { ...DEFAULT_USER_SETTINGS, ...settings }; } catch (error) { @@ -114,6 +142,31 @@ export class SettingsManager { } } + /** + * Migrate settings from an older version to the current version + */ + private migrateSettings(settings: UserSettings, fromVersion: number): UserSettings { + let migrated = { ...settings }; + + // Migration from version 1 to 2: Add new Grok 4.1 and Grok 4 Fast models + if (fromVersion < 2) { + const defaultModels = DEFAULT_USER_SETTINGS.models || []; + const existingModels = new Set(migrated.models || []); + + // Add any new models that don't exist in user's current list + const newModels = defaultModels.filter(model => !existingModels.has(model)); + + // Prepend new models to the list (newest models first) + migrated.models = [...newModels, ...(migrated.models || [])]; + } + + // Add future migrations here: + // if (fromVersion < 3) { ... } + + migrated.settingsVersion = SETTINGS_VERSION; + return migrated; + } + /** * Save user settings to ~/.grok/user-settings.json */