@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
169 lines • 6.15 kB
JavaScript
import { readFile, stat } from 'node:fs/promises';
import { extname, resolve } from 'node:path';
import { BINARY_FILE_EXTENSIONS, MAX_FILE_TAG_SIZE_BYTES } from '../constants.js';
/**
* Load file content with optional line range
* Silently handles errors - returns success: false instead of throwing
*/
export async function loadFileContent(filePath, lineRange) {
try {
const absPath = resolve(filePath);
// Check if file exists and get stats
const fileStats = await stat(absPath);
// Check if it's a file (not directory)
if (!fileStats.isFile()) {
return {
success: false,
error: 'Path is not a file',
metadata: {
path: filePath,
absolutePath: absPath,
size: 0,
lineCount: 0,
lineRange,
tokens: 0,
},
};
}
// Binary extension check
const ext = extname(absPath).toLowerCase();
if (BINARY_FILE_EXTENSIONS.has(ext)) {
const fileType = ext.slice(1).toUpperCase();
const metadataContent = [
`[Binary file: ${filePath}]`,
`Type: ${fileType}`,
`Size: ${fileStats.size.toLocaleString()} bytes (${formatBytes(fileStats.size)})`,
`Last Modified: ${fileStats.mtime.toISOString()}`,
'',
'(Binary files cannot be included as text content)',
].join('\n');
return {
success: true,
content: metadataContent,
metadata: {
path: filePath,
absolutePath: absPath,
size: fileStats.size,
lineCount: 0,
lineRange,
tokens: Math.ceil(metadataContent.length / 4),
},
};
}
// Size check
if (fileStats.size > MAX_FILE_TAG_SIZE_BYTES && !lineRange) {
const estimatedLines = Math.round(fileStats.size / 40);
const metadataContent = [
`[Large file: ${filePath}]`,
`Size: ${fileStats.size.toLocaleString()} bytes (${formatBytes(fileStats.size)})`,
`Lines: ~${estimatedLines.toLocaleString()}`,
`Last Modified: ${fileStats.mtime.toISOString()}`,
'',
`(File exceeds ${formatBytes(MAX_FILE_TAG_SIZE_BYTES)} limit for inline tagging. Use @file:1-100 to tag specific line ranges)`,
].join('\n');
return {
success: true,
content: metadataContent,
metadata: {
path: filePath,
absolutePath: absPath,
size: fileStats.size,
lineCount: 0,
lineRange,
tokens: Math.ceil(metadataContent.length / 4),
},
};
}
// Read file content
let content;
try {
content = await readFile(absPath, 'utf-8');
}
catch {
// File might be binary or unreadable
return {
success: false,
error: 'Failed to read file (might be binary)',
metadata: {
path: filePath,
absolutePath: absPath,
size: fileStats.size,
lineCount: 0,
lineRange,
tokens: 0,
},
};
}
// Split into lines
const allLines = content.split('\n');
const totalLines = allLines.length;
// Extract line range if specified
let selectedLines;
let actualLineRange;
if (lineRange) {
const start = Math.max(1, lineRange.start);
const end = lineRange.end ? Math.min(totalLines, lineRange.end) : start;
// Validate range
if (start > totalLines) {
// Invalid range, return empty
selectedLines = [];
}
else {
// Arrays are 0-indexed, but line numbers are 1-indexed
selectedLines = allLines.slice(start - 1, end);
actualLineRange = { start, end: lineRange.end ? end : undefined };
}
}
else {
// No line range, use all lines
selectedLines = allLines;
}
// Format with path header (no line numbers)
const formattedContent = formatFileContent(selectedLines, filePath);
// Calculate metadata
const size = content.length;
const tokens = Math.ceil(formattedContent.length / 4); // Rough token estimate
return {
success: true,
content: formattedContent,
metadata: {
path: filePath,
absolutePath: absPath,
size,
lineCount: selectedLines.length,
lineRange: actualLineRange,
tokens,
},
};
}
catch (error) {
// File doesn't exist or other error
const absPath = resolve(filePath);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
metadata: {
path: filePath,
absolutePath: absPath,
size: 0,
lineCount: 0,
lineRange,
tokens: 0,
},
};
}
}
/**
* Format file content with path header (no line numbers for clean content-based editing)
*/
function formatFileContent(lines, filePath) {
return `Path: ${filePath}\n\n${lines.join('\n')}`;
}
function formatBytes(bytes) {
if (bytes < 1024)
return `${bytes} B`;
if (bytes < 1024 * 1024)
return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
//# sourceMappingURL=file-content-loader.js.map