UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

118 lines 4.23 kB
import { exec } from 'node:child_process'; import { promisify } from 'node:util'; import { BUFFER_FILE_LIST_BYTES, CACHE_FILE_LIST_TTL_MS } from '../constants.js'; import { formatError } from './error-formatter.js'; import { fuzzyScoreFilePath } from './fuzzy-matching.js'; import { loadGitignore } from './gitignore-loader.js'; import { getLogger } from './logging/index.js'; const execAsync = promisify(exec); let fileListCache = null; /** * Get list of all files in the project (respecting gitignore) */ async function getAllFiles(cwd) { // Check cache const now = Date.now(); if (fileListCache && now - fileListCache.timestamp < CACHE_FILE_LIST_TTL_MS) { return fileListCache.files; } try { const ig = loadGitignore(cwd); // Use find to list all files, excluding common large directories const { stdout } = await execAsync(`find . -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/dist/*" -not -path "*/build/*" -not -path "*/coverage/*" -not -path "*/.next/*" -not -path "*/.nuxt/*" -not -path "*/out/*" -not -path "*/.cache/*"`, { cwd, maxBuffer: BUFFER_FILE_LIST_BYTES }); const allFiles = stdout .trim() .split('\n') .filter(Boolean) .map(line => line.replace(/^\.\//, '')) // Remove leading "./" .filter(file => !ig.ignores(file)); // Filter by gitignore // Update cache fileListCache = { files: allFiles, timestamp: now, }; return allFiles; } catch (error) { // If find fails, return empty array const logger = getLogger(); logger.error({ error: formatError(error) }, 'Failed to list files'); return []; } } /** * Extract the current @mention being typed at cursor position * Returns the mention text and its position in the input */ export function getCurrentFileMention(input, cursorPosition) { const pos = cursorPosition ?? input.length; // Find the last @ before cursor let startIndex = -1; for (let i = pos - 1; i >= 0; i--) { if (input[i] === '@') { startIndex = i; break; } // Stop if we hit whitespace (except for path separators) if (input[i] === ' ' || input[i] === '\t' || input[i] === '\n') { break; } } if (startIndex === -1) { return null; } // Find the end of the mention (next whitespace or end of string) let endIndex = pos; for (let i = pos; i < input.length; i++) { if (input[i] === ' ' || input[i] === '\t' || input[i] === '\n' || input[i] === '@') { break; } endIndex = i + 1; } // Extract mention text (without the @) const fullText = input.substring(startIndex, endIndex); const mention = fullText.substring(1); // Remove @ prefix // Remove line range suffix if present (e.g., ":10-20") const mentionWithoutRange = mention.replace(/:\d+(-\d+)?$/, ''); return { mention: mentionWithoutRange, startIndex, endIndex, }; } /** * Get file completions for a partial path */ export async function getFileCompletions(partialPath, cwd, maxResults = 20) { // Get all files const allFiles = await getAllFiles(cwd); // Score each file const scoredFiles = allFiles .map(file => ({ path: file, displayPath: file.length > 50 ? '...' + file.slice(-47) : file, score: fuzzyScoreFilePath(file, partialPath), isDirectory: false, // We're only listing files, not directories })) .filter(f => f.score > 0) // Only include matches .sort((a, b) => { // Sort by score (descending) if (b.score !== a.score) { return b.score - a.score; } // If scores are equal, sort alphabetically return a.path.localeCompare(b.path); }) .slice(0, maxResults); // Limit results return scoredFiles; } /** * Clear the file list cache (useful for testing or when files change) */ export function clearFileListCache() { fileListCache = null; } //# sourceMappingURL=file-autocomplete.js.map