From 597637ddb85e5a019d0d43b460ad5b54564c4933 Mon Sep 17 00:00:00 2001 From: spokV Date: Sun, 31 Aug 2025 09:55:15 +0300 Subject: [PATCH 1/5] feat: Add file attachment support with @ symbol (Claude Code style) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement file picker functionality that allows users to attach files to prompts using the @ symbol: • Type @ to trigger intelligent file picker with fuzzy search • Navigate with arrow keys, select with Tab/Enter • Shows file content to AI while keeping chat history clean • Smart scrolling for long file lists with position indicators • Atomic cursor positioning to ensure proper input flow New components: - FilePicker: Scrollable file selection UI with visual feedback - FileFinder: Efficient file scanning with caching and security checks - Enhanced input hook: Atomic input/cursor positioning method 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/hooks/use-enhanced-input.ts | 10 ++ src/hooks/use-input-handler.ts | 136 ++++++++++++++++++- src/ui/components/chat-interface.tsx | 24 +++- src/ui/components/file-picker.tsx | 92 +++++++++++++ src/utils/file-finder.ts | 191 +++++++++++++++++++++++++++ 5 files changed, 449 insertions(+), 4 deletions(-) create mode 100644 src/ui/components/file-picker.tsx create mode 100644 src/utils/file-finder.ts diff --git a/src/hooks/use-enhanced-input.ts b/src/hooks/use-enhanced-input.ts index 299f27d..da6e8c5 100644 --- a/src/hooks/use-enhanced-input.ts +++ b/src/hooks/use-enhanced-input.ts @@ -36,6 +36,7 @@ export interface EnhancedInputHook { isMultiline: boolean; setInput: (text: string) => void; setCursorPosition: (position: number) => void; + setInputWithCursor: (text: string, position: number) => void; clearInput: () => void; insertAtCursor: (text: string) => void; resetHistory: () => void; @@ -81,6 +82,14 @@ export function useEnhancedInput({ setCursorPositionState(Math.max(0, Math.min(input.length, position))); }, [input.length]); + const setInputWithCursor = useCallback((text: string, position: number) => { + setInputState(text); + setCursorPositionState(Math.max(0, Math.min(text.length, position))); + if (!isNavigatingHistory()) { + setOriginalInput(text); + } + }, [isNavigatingHistory, setOriginalInput]); + const clearInput = useCallback(() => { setInputState(""); setCursorPositionState(0); @@ -290,6 +299,7 @@ export function useEnhancedInput({ isMultiline: isMultilineRef.current, setInput, setCursorPosition, + setInputWithCursor, clearInput, insertAtCursor, resetHistory, diff --git a/src/hooks/use-input-handler.ts b/src/hooks/use-input-handler.ts index 847d137..55cfd4d 100644 --- a/src/hooks/use-input-handler.ts +++ b/src/hooks/use-input-handler.ts @@ -6,6 +6,7 @@ import { useEnhancedInput, Key } from "./use-enhanced-input"; import { filterCommandSuggestions } from "../ui/components/command-suggestions"; import { loadModelConfig, updateCurrentModel } from "../utils/model-config"; +import { FileFinder, FileSuggestion } from "../utils/file-finder"; interface UseInputHandlerProps { agent: GrokAgent; @@ -30,6 +31,12 @@ interface ModelOption { model: string; } + +interface AttachedFile { + path: string; + content: string; +} + export function useInputHandler({ agent, chatHistory, @@ -52,6 +59,13 @@ export function useInputHandler({ const sessionFlags = confirmationService.getSessionFlags(); return sessionFlags.allOperations; }); + + // File selection state + const [showFilePicker, setShowFilePicker] = useState(false); + const [selectedFileIndex, setSelectedFileIndex] = useState(0); + const [fileQuery, setFileQuery] = useState(""); + const [attachedFiles, setAttachedFiles] = useState([]); + const [fileFinder] = useState(() => new FileFinder()); const handleSpecialKey = (key: Key): boolean => { // Don't handle input if confirmation dialog is active @@ -87,6 +101,12 @@ export function useInputHandler({ setSelectedModelIndex(0); return true; } + if (showFilePicker) { + setShowFilePicker(false); + setSelectedFileIndex(0); + setFileQuery(""); + return true; + } if (isProcessing || isStreaming) { agent.abortCurrentOperation(); setIsProcessing(false); @@ -166,6 +186,40 @@ export function useInputHandler({ return true; } } + + // Handle file picker navigation + if (showFilePicker) { + const fileSuggestions = fileFinder.getFileSuggestions(fileQuery); + + if (fileSuggestions.length === 0) { + setShowFilePicker(false); + setSelectedFileIndex(0); + setFileQuery(""); + return false; + } + + if (key.upArrow) { + setSelectedFileIndex((prev) => + prev === 0 ? fileSuggestions.length - 1 : prev - 1 + ); + return true; + } + if (key.downArrow) { + setSelectedFileIndex((prev) => (prev + 1) % fileSuggestions.length); + return true; + } + if (key.tab || key.return) { + const selectedFile = fileSuggestions[selectedFileIndex]; + if (!selectedFile.isDirectory) { + // Attach file to prompt + attachFileToPrompt(selectedFile.relativePath); + } + setShowFilePicker(false); + setSelectedFileIndex(0); + setFileQuery(""); + return true; + } + } return false; // Let default handling proceed }; @@ -189,10 +243,68 @@ export function useInputHandler({ if (newInput.startsWith("/")) { setShowCommandSuggestions(true); setSelectedCommandIndex(0); + setShowFilePicker(false); + setFileQuery(""); } else { setShowCommandSuggestions(false); setSelectedCommandIndex(0); } + + // Handle @ file selection + const atIndex = newInput.lastIndexOf("@"); + if (atIndex !== -1) { + const afterAt = newInput.slice(atIndex + 1); + // Check if we're at the end or followed by whitespace + const isEndOfInput = atIndex + afterAt.length + 1 === newInput.length; + const isFollowedBySpace = !isEndOfInput && /\s/.test(newInput[atIndex + afterAt.length + 1]); + + if (isEndOfInput || isFollowedBySpace) { + setShowFilePicker(true); + setFileQuery(afterAt); + setSelectedFileIndex(0); + setShowCommandSuggestions(false); + } else { + setShowFilePicker(false); + setFileQuery(""); + } + } else { + setShowFilePicker(false); + setFileQuery(""); + } + }; + + const attachFileToPrompt = async (relativePath: string) => { + try { + const content = await fileFinder.readFileContent(relativePath); + if (content) { + const newAttachedFile: AttachedFile = { + path: relativePath, + content + }; + setAttachedFiles(prev => [...prev, newAttachedFile]); + + // Replace @query with @filename in input and position cursor at end + const atIndex = input.lastIndexOf("@"); + if (atIndex !== -1) { + const beforeAt = input.slice(0, atIndex); + // Find any content after the current @query + const afterAtPart = input.slice(atIndex); + const spaceIndex = afterAtPart.search(/\s/); + const afterQuery = spaceIndex !== -1 ? afterAtPart.slice(spaceIndex) : ""; + + // Build new input with @filename and ensure space after + const newInput = beforeAt + `@${relativePath} ` + afterQuery; + // Set input and cursor position atomically + setInputWithCursor(newInput, newInput.length); + } + } + } catch (error) { + console.error("Error attaching file:", error); + } + }; + + const removeAttachedFile = (pathToRemove: string) => { + setAttachedFiles(prev => prev.filter(file => file.path !== pathToRemove)); }; const { @@ -200,6 +312,7 @@ export function useInputHandler({ cursorPosition, setInput, setCursorPosition, + setInputWithCursor, clearInput, resetHistory, handleInput, @@ -608,12 +721,26 @@ Respond with ONLY the commit message, no additional text.`; }; const processUserMessage = async (userInput: string) => { + // Display user input without file content in chat history const userEntry: ChatEntry = { type: "user", content: userInput, timestamp: new Date(), }; setChatHistory((prev) => [...prev, userEntry]); + + // Prepare the actual message content with attached files for AI processing + let messageContent = userInput; + + if (attachedFiles.length > 0) { + messageContent += "\n\n--- Attached Files ---\n"; + for (const file of attachedFiles) { + messageContent += `\n**File: ${file.path}**\n\`\`\`\n${file.content}\n\`\`\`\n`; + } + } + + // Clear attached files after processing + setAttachedFiles([]); setIsProcessing(true); clearInput(); @@ -622,7 +749,7 @@ Respond with ONLY the commit message, no additional text.`; setIsStreaming(true); let streamingEntry: ChatEntry | null = null; - for await (const chunk of agent.processUserMessageStream(userInput)) { + for await (const chunk of agent.processUserMessageStream(messageContent)) { switch (chunk.type) { case "content": if (chunk.content) { @@ -748,5 +875,12 @@ Respond with ONLY the commit message, no additional text.`; availableModels, agent, autoEditEnabled, + // File picker state + showFilePicker, + selectedFileIndex, + fileQuery, + attachedFiles, + fileSuggestions: fileFinder.getFileSuggestions(fileQuery), + removeAttachedFile, }; } diff --git a/src/ui/components/chat-interface.tsx b/src/ui/components/chat-interface.tsx index b4490ca..b65d8a7 100644 --- a/src/ui/components/chat-interface.tsx +++ b/src/ui/components/chat-interface.tsx @@ -7,6 +7,7 @@ import { CommandSuggestions } from "./command-suggestions"; import { ModelSelection } from "./model-selection"; import { ChatHistory } from "./chat-history"; import { ChatInput } from "./chat-input"; +import { FilePicker } from "./file-picker"; import { MCPStatus } from "./mcp-status"; import ConfirmationDialog from "./confirmation-dialog"; import { @@ -44,6 +45,13 @@ function ChatInterfaceWithAgent({ agent }: { agent: GrokAgent }) { commandSuggestions, availableModels, autoEditEnabled, + // File picker state + showFilePicker, + selectedFileIndex, + fileQuery, + attachedFiles, + fileSuggestions, + removeAttachedFile, } = useInputHandler({ agent, chatHistory, @@ -166,12 +174,15 @@ function ChatInterfaceWithAgent({ agent }: { agent: GrokAgent }) { 2. Be specific for the best results. - 3. Create GROK.md files to customize your interactions with Grok. + 3. Use @ to attach files to your prompt (e.g., "@package.json"). - 4. Press Shift+Tab to toggle auto-edit mode. + 4. Create GROK.md files to customize your interactions with Grok. - 5. /help for more information. + + 5. Press Shift+Tab to toggle auto-edit mode. + + 6. /help for more information. )} @@ -243,6 +254,13 @@ function ChatInterfaceWithAgent({ agent }: { agent: GrokAgent }) { isVisible={showModelSelection} currentModel={agent.getCurrentModel()} /> + + )} diff --git a/src/ui/components/file-picker.tsx b/src/ui/components/file-picker.tsx new file mode 100644 index 0000000..98457ec --- /dev/null +++ b/src/ui/components/file-picker.tsx @@ -0,0 +1,92 @@ +import React from "react"; +import { Box, Text } from "ink"; +import path from "path"; +import { FileSuggestion } from "../../utils/file-finder"; + +interface FilePickerProps { + suggestions: FileSuggestion[]; + selectedIndex: number; + query: string; + isVisible: boolean; +} + +export function FilePicker({ + suggestions, + selectedIndex, + query, + isVisible, +}: FilePickerProps) { + if (!isVisible || suggestions.length === 0) { + return null; + } + + const maxVisible = 8; + const startIndex = Math.max(0, selectedIndex - Math.floor(maxVisible / 2)); + const endIndex = Math.min(suggestions.length, startIndex + maxVisible); + const adjustedStartIndex = Math.max(0, endIndex - maxVisible); + + const visibleSuggestions = suggestions.slice(adjustedStartIndex, endIndex); + const adjustedSelectedIndex = selectedIndex - adjustedStartIndex; + + return ( + + + + 📁 File Selection {query && `(${query})`} + + {suggestions.length > maxVisible && ( + + {selectedIndex + 1}/{suggestions.length} files + + )} + + + {adjustedStartIndex > 0 && ( + + + ↑ {adjustedStartIndex} more above + + + )} + + {visibleSuggestions.map((suggestion, index) => { + const isSelected = index === adjustedSelectedIndex; + const icon = suggestion.isDirectory ? "📂" : "📄"; + + return ( + + + {isSelected ? "❯ " : " "} + {icon} {suggestion.relativePath} + + + ); + })} + + {endIndex < suggestions.length && ( + + + ↓ {suggestions.length - endIndex} more below + + + )} + + + + ↑/↓ Navigate • Tab/Enter Select • Esc Cancel + + + + ); +} \ No newline at end of file diff --git a/src/utils/file-finder.ts b/src/utils/file-finder.ts new file mode 100644 index 0000000..ebee548 --- /dev/null +++ b/src/utils/file-finder.ts @@ -0,0 +1,191 @@ +import * as fs from "fs"; +import * as path from "path"; + +export interface FileSuggestion { + file: string; + relativePath: string; + isDirectory: boolean; +} + +export class FileFinder { + private fileCache: Map = new Map(); + private maxDepth: number = 3; + private maxFiles: number = 200; + + constructor(private workingDirectory: string = process.cwd()) {} + + /** + * Get file suggestions based on query + */ + getFileSuggestions(query: string): FileSuggestion[] { + const cacheKey = this.workingDirectory; + + // Use cached results if available + if (!this.fileCache.has(cacheKey)) { + this.buildFileCache(); + } + + const allFiles = this.fileCache.get(cacheKey) || []; + + if (!query) { + return allFiles.slice(0, 20); // Return first 20 files when no query + } + + // Filter files based on query + const filtered = allFiles.filter(file => + file.relativePath.toLowerCase().includes(query.toLowerCase()) || + path.basename(file.relativePath).toLowerCase().includes(query.toLowerCase()) + ); + + // Sort by relevance (exact matches first, then partial matches) + return filtered.sort((a, b) => { + const aBasename = path.basename(a.relativePath).toLowerCase(); + const bBasename = path.basename(b.relativePath).toLowerCase(); + const queryLower = query.toLowerCase(); + + // Exact basename matches first + if (aBasename === queryLower && bBasename !== queryLower) return -1; + if (bBasename === queryLower && aBasename !== queryLower) return 1; + + // Basename starts with query + if (aBasename.startsWith(queryLower) && !bBasename.startsWith(queryLower)) return -1; + if (bBasename.startsWith(queryLower) && !aBasename.startsWith(queryLower)) return 1; + + // Path starts with query + if (a.relativePath.toLowerCase().startsWith(queryLower) && + !b.relativePath.toLowerCase().startsWith(queryLower)) return -1; + if (b.relativePath.toLowerCase().startsWith(queryLower) && + !a.relativePath.toLowerCase().startsWith(queryLower)) return 1; + + // Shorter paths first + return a.relativePath.length - b.relativePath.length; + }).slice(0, 50); + } + + /** + * Build file cache by recursively scanning directories + */ + private buildFileCache(): void { + const files: FileSuggestion[] = []; + const visited = new Set(); + + const shouldIgnore = (filePath: string): boolean => { + const basename = path.basename(filePath); + const ignoredPatterns = [ + /^\./, // Hidden files + /node_modules/, + /\.git$/, + /dist$/, + /build$/, + /coverage$/, + /\.next$/, + /\.nuxt$/, + /\.output$/, + /target$/, // Rust + /__pycache__$/, + /\.pyc$/, + /\.DS_Store$/, + /Thumbs\.db$/, + ]; + + return ignoredPatterns.some(pattern => pattern.test(basename) || pattern.test(filePath)); + }; + + const scanDirectory = (dir: string, depth: number = 0): void => { + if (depth > this.maxDepth || files.length > this.maxFiles) return; + + try { + const realPath = fs.realpathSync(dir); + if (visited.has(realPath)) return; // Avoid circular references + visited.add(realPath); + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + const relativePath = path.relative(this.workingDirectory, fullPath); + + if (shouldIgnore(fullPath)) continue; + + const suggestion: FileSuggestion = { + file: entry.name, + relativePath: relativePath || entry.name, + isDirectory: entry.isDirectory(), + }; + + files.push(suggestion); + + // Recursively scan subdirectories + if (entry.isDirectory() && depth < this.maxDepth) { + scanDirectory(fullPath, depth + 1); + } + } + } catch (error) { + // Silently ignore errors (permission issues, etc.) + } + }; + + scanDirectory(this.workingDirectory); + this.fileCache.set(this.workingDirectory, files); + } + + /** + * Clear the file cache (useful when files change) + */ + clearCache(): void { + this.fileCache.clear(); + } + + /** + * Read file content for attaching to prompt + */ + async readFileContent(relativePath: string): Promise { + try { + const fullPath = path.resolve(this.workingDirectory, relativePath); + + // Security check - ensure file is within working directory + const resolvedPath = path.resolve(fullPath); + const resolvedWorkDir = path.resolve(this.workingDirectory); + if (!resolvedPath.startsWith(resolvedWorkDir)) { + throw new Error("File access outside working directory not allowed"); + } + + const stats = fs.statSync(fullPath); + if (!stats.isFile()) { + return null; // Not a file + } + + // Don't read very large files (>1MB) + if (stats.size > 1024 * 1024) { + return `[File too large: ${relativePath} (${Math.round(stats.size / 1024)}KB)]`; + } + + // Don't read binary files + if (this.isBinaryFile(fullPath)) { + return `[Binary file: ${relativePath}]`; + } + + const content = fs.readFileSync(fullPath, "utf8"); + return content; + } catch (error) { + return `[Error reading file: ${relativePath}]`; + } + } + + /** + * Simple binary file detection + */ + private isBinaryFile(filePath: string): boolean { + const binaryExtensions = [ + ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".ico", ".svg", + ".mp3", ".mp4", ".avi", ".mov", ".wmv", ".flv", + ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", + ".zip", ".rar", ".7z", ".tar", ".gz", + ".exe", ".dll", ".so", ".dylib", + ".woff", ".woff2", ".ttf", ".otf", + ]; + + const ext = path.extname(filePath).toLowerCase(); + return binaryExtensions.includes(ext); + } +} \ No newline at end of file From 98a2b754d5dc37f0defe0e194b76fdbf898e4ad0 Mon Sep 17 00:00:00 2001 From: spokV Date: Sun, 31 Aug 2025 10:10:14 +0300 Subject: [PATCH 2/5] Remove file/folder icons from file picker for cleaner UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove emoji icons from file picker display - Show directories with trailing / instead of folder icon - Matches Claude's clean file selector interface 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/ui/components/file-picker.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/components/file-picker.tsx b/src/ui/components/file-picker.tsx index 98457ec..7b9e2ab 100644 --- a/src/ui/components/file-picker.tsx +++ b/src/ui/components/file-picker.tsx @@ -39,7 +39,7 @@ export function FilePicker({ > - 📁 File Selection {query && `(${query})`} + File Selection {query && `(${query})`} {suggestions.length > maxVisible && ( @@ -58,7 +58,7 @@ export function FilePicker({ {visibleSuggestions.map((suggestion, index) => { const isSelected = index === adjustedSelectedIndex; - const icon = suggestion.isDirectory ? "📂" : "📄"; + const displayPath = suggestion.isDirectory ? `${suggestion.relativePath}/` : suggestion.relativePath; return ( @@ -68,7 +68,7 @@ export function FilePicker({ bold={isSelected} > {isSelected ? "❯ " : " "} - {icon} {suggestion.relativePath} + {displayPath} ); From cde1be13a0e5c65a140c38154a9306193e272e81 Mon Sep 17 00:00:00 2001 From: spokV Date: Sun, 31 Aug 2025 11:43:27 +0300 Subject: [PATCH 3/5] add defines for depth and size. add cache refresh --- src/hooks/use-input-handler.ts | 48 ++++++++++++++++------------------ src/utils/file-finder.ts | 28 ++++++++++++++------ 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/hooks/use-input-handler.ts b/src/hooks/use-input-handler.ts index 55cfd4d..a9f1de9 100644 --- a/src/hooks/use-input-handler.ts +++ b/src/hooks/use-input-handler.ts @@ -250,27 +250,23 @@ export function useInputHandler({ setSelectedCommandIndex(0); } - // Handle @ file selection - const atIndex = newInput.lastIndexOf("@"); + // Handle @ file selection (only the current @-token up to whitespace) + const searchPos = Math.max(0, cursorPosition - 1); + const atIndex = newInput.lastIndexOf("@", searchPos); if (atIndex !== -1) { - const afterAt = newInput.slice(atIndex + 1); - // Check if we're at the end or followed by whitespace - const isEndOfInput = atIndex + afterAt.length + 1 === newInput.length; - const isFollowedBySpace = !isEndOfInput && /\s/.test(newInput[atIndex + afterAt.length + 1]); - - if (isEndOfInput || isFollowedBySpace) { + const segment = newInput.slice(atIndex + 1, cursorPosition); + const containsWhitespace = /\s/.test(segment); + + if (!containsWhitespace) { setShowFilePicker(true); - setFileQuery(afterAt); + setFileQuery(segment); setSelectedFileIndex(0); setShowCommandSuggestions(false); - } else { - setShowFilePicker(false); - setFileQuery(""); + return; } - } else { - setShowFilePicker(false); - setFileQuery(""); } + setShowFilePicker(false); + setFileQuery(""); }; const attachFileToPrompt = async (relativePath: string) => { @@ -284,18 +280,20 @@ export function useInputHandler({ setAttachedFiles(prev => [...prev, newAttachedFile]); // Replace @query with @filename in input and position cursor at end - const atIndex = input.lastIndexOf("@"); + const searchPos = Math.max(0, cursorPosition - 1); + const atIndex = input.lastIndexOf("@", searchPos); if (atIndex !== -1) { const beforeAt = input.slice(0, atIndex); - // Find any content after the current @query - const afterAtPart = input.slice(atIndex); - const spaceIndex = afterAtPart.search(/\s/); - const afterQuery = spaceIndex !== -1 ? afterAtPart.slice(spaceIndex) : ""; - - // Build new input with @filename and ensure space after - const newInput = beforeAt + `@${relativePath} ` + afterQuery; - // Set input and cursor position atomically - setInputWithCursor(newInput, newInput.length); + // Find end of current token (first whitespace after '@') + const rest = input.slice(atIndex + 1); + const whitespaceRel = rest.search(/\s/); + const tokenEnd = whitespaceRel === -1 ? input.length : atIndex + 1 + whitespaceRel; + + const afterQuery = input.slice(tokenEnd); + const insertion = `@${relativePath} `; + const newInput = beforeAt + insertion + afterQuery; + const newCursor = beforeAt.length + insertion.length; + setInputWithCursor(newInput, newCursor); } } } catch (error) { diff --git a/src/utils/file-finder.ts b/src/utils/file-finder.ts index ebee548..8319cd6 100644 --- a/src/utils/file-finder.ts +++ b/src/utils/file-finder.ts @@ -9,10 +9,19 @@ export interface FileSuggestion { export class FileFinder { private fileCache: Map = new Map(); - private maxDepth: number = 3; - private maxFiles: number = 200; - - constructor(private workingDirectory: string = process.cwd()) {} + private maxDepth: number = 4; + private maxFiles: number = 5000; + private includeDotfiles: boolean = false; + + constructor(private workingDirectory: string = process.cwd()) { + const depthEnv = parseInt(process.env.GROK_FILEPICKER_MAX_DEPTH || "", 10); + const filesEnv = parseInt(process.env.GROK_FILEPICKER_MAX_FILES || "", 10); + const includeDotsEnv = (process.env.GROK_FILEPICKER_INCLUDE_DOTFILES || "").toLowerCase(); + + if (!Number.isNaN(depthEnv) && depthEnv > 0) this.maxDepth = depthEnv; + if (!Number.isNaN(filesEnv) && filesEnv > 0) this.maxFiles = filesEnv; + this.includeDotfiles = includeDotsEnv === "1" || includeDotsEnv === "true"; + } /** * Get file suggestions based on query @@ -72,7 +81,8 @@ export class FileFinder { const shouldIgnore = (filePath: string): boolean => { const basename = path.basename(filePath); const ignoredPatterns = [ - /^\./, // Hidden files + // Hidden files (optional) + ...(this.includeDotfiles ? [] : [/^\./]), /node_modules/, /\.git$/, /dist$/, @@ -87,8 +97,10 @@ export class FileFinder { /\.DS_Store$/, /Thumbs\.db$/, ]; - - return ignoredPatterns.some(pattern => pattern.test(basename) || pattern.test(filePath)); + + return ignoredPatterns.some( + (pattern) => pattern.test(basename) || pattern.test(filePath) + ); }; const scanDirectory = (dir: string, depth: number = 0): void => { @@ -188,4 +200,4 @@ export class FileFinder { const ext = path.extname(filePath).toLowerCase(); return binaryExtensions.includes(ext); } -} \ No newline at end of file +} From d9ed2e79c446db225cceb5b6cf8a210c9470fbdc Mon Sep 17 00:00:00 2001 From: spokV Date: Sun, 31 Aug 2025 11:45:45 +0300 Subject: [PATCH 4/5] build: add binary build script and pkg config --- package-lock.json | 4 ++-- package.json | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index fcd126f..5700cd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vibe-kit/grok-cli", - "version": "0.0.17", + "version": "0.0.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vibe-kit/grok-cli", - "version": "0.0.17", + "version": "0.0.23", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.0", diff --git a/package.json b/package.json index efbbeb2..1e078e7 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "dev": "tsx src/index.ts", "start": "node dist/index.js", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "build:binary": "npm run build && pkg . --targets node18-linux-x64 --output grok-linux" }, "keywords": [ "cli", @@ -51,5 +52,17 @@ "engines": { "node": ">=16.0.0" }, - "preferGlobal": true + "preferGlobal": true, + "publishConfig": { + "access": "public" + }, + "pkg": { + "scripts": ["dist/**/*.js"], + "targets": ["node18-linux-x64"], + "outputPath": "bin", + "options": [ + "--no-bytecode", + "--public-packages=*" + ] + } } From da238904eb17e2afc48676943511e88eed8d43ed Mon Sep 17 00:00:00 2001 From: spokV Date: Sun, 31 Aug 2025 11:50:09 +0300 Subject: [PATCH 5/5] Revert "build: add binary build script and pkg config" This reverts commit d9ed2e79c446db225cceb5b6cf8a210c9470fbdc. --- package-lock.json | 4 ++-- package.json | 17 ++--------------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5700cd8..fcd126f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vibe-kit/grok-cli", - "version": "0.0.23", + "version": "0.0.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vibe-kit/grok-cli", - "version": "0.0.23", + "version": "0.0.17", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.0", diff --git a/package.json b/package.json index 1e078e7..efbbeb2 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,7 @@ "dev": "tsx src/index.ts", "start": "node dist/index.js", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", - "typecheck": "tsc --noEmit", - "build:binary": "npm run build && pkg . --targets node18-linux-x64 --output grok-linux" + "typecheck": "tsc --noEmit" }, "keywords": [ "cli", @@ -52,17 +51,5 @@ "engines": { "node": ">=16.0.0" }, - "preferGlobal": true, - "publishConfig": { - "access": "public" - }, - "pkg": { - "scripts": ["dist/**/*.js"], - "targets": ["node18-linux-x64"], - "outputPath": "bin", - "options": [ - "--no-bytecode", - "--public-packages=*" - ] - } + "preferGlobal": true }