packfs-core
Version:
Semantic filesystem operations for LLM agent frameworks with natural language understanding. See LLM_AGENT_GUIDE.md for copy-paste examples.
1,253 lines (1,252 loc) • 60.1 kB
JavaScript
"use strict";
/**
* Production-ready disk-based semantic filesystem backend
* Implements persistent semantic indexing and full LSFS operations
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiskSemanticBackend = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const interface_js_1 = require("./interface.js");
const intent_processor_js_1 = require("./intent-processor.js");
const logger_js_1 = require("../core/logger.js");
const error_recovery_js_1 = require("./error-recovery.js");
/**
* Production disk-based semantic filesystem with persistent indexing
*/
class DiskSemanticBackend extends interface_js_1.SemanticFileSystemInterface {
constructor(basePath, config) {
super(config);
this.indexLoaded = false;
this.indexVersion = '1.0.0';
this.excludedDirectories = new Set([
'node_modules',
'.git',
'.svn',
'.hg',
'.DS_Store',
'dist',
'build',
'coverage',
'.next',
'.nuxt',
'.cache',
'vendor',
'bower_components',
'__pycache__',
'.pytest_cache',
'.mypy_cache',
'.tox',
'.packfs' // Exclude our own directory
]);
this.maxIndexingDepth = 10; // Maximum directory depth to prevent stack overflow
this.visitedPaths = new Set(); // Track visited paths to prevent cycles
this.basePath = basePath;
this.indexPath = (0, path_1.join)(basePath, '.packfs', 'semantic-index.json');
this.maxFileSize = 50 * 1024 * 1024; // 50MB max file size for indexing
this.logger = logger_js_1.Logger.getInstance().createChildLogger('DiskSemanticBackend');
this.errorRecovery = new error_recovery_js_1.ErrorRecoveryEngine(basePath);
this.index = {
version: this.indexVersion,
created: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
entries: {},
keywordMap: {},
};
this.logger.info(`Initialized semantic backend at ${basePath}`);
}
/**
* Initialize the semantic backend and load/create index
*/
async initialize() {
this.logger.info('Starting semantic backend initialization');
try {
// Ensure base directory exists
await fs_1.promises.mkdir(this.basePath, { recursive: true });
await fs_1.promises.mkdir((0, path_1.dirname)(this.indexPath), { recursive: true });
this.logger.debug('Created directories');
// Load or create semantic index
await this.loadIndex();
// Perform incremental index update
await this.updateIndexIfNeeded();
this.indexLoaded = true;
this.logger.info('Semantic backend initialization complete');
}
catch (error) {
this.logger.error('Failed to initialize semantic disk backend', { error });
throw new Error(`Failed to initialize semantic disk backend: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async accessFile(intent) {
this.logger.debug('Processing file access intent', intent);
await this.ensureIndexLoaded();
// Extract working directory from intent if provided
const workingDirectory = intent.options?.workingDirectory;
const effectiveBasePath = workingDirectory || this.basePath;
// Handle direct path access first
if (intent.target.path) {
const relativePath = this.normalizePath(intent.target.path);
// If working directory is provided, use it for file operations
if (workingDirectory) {
return this.handleSingleFileAccessWithBasePath(relativePath, intent, effectiveBasePath);
}
return this.handleSingleFileAccess(relativePath, intent);
}
const targets = await intent_processor_js_1.FileTargetProcessor.resolveTarget(intent.target, this.basePath);
// Handle single file operations
if (targets.length === 1 && targets[0] && !targets[0].startsWith('__')) {
const filePath = this.normalizePath(targets[0]);
return this.handleSingleFileAccess(filePath, intent);
}
// Handle semantic/criteria-based targeting
const matchingFiles = await this.findFilesByTarget(intent.target);
// For verify_exists, we always return success with exists status
if (intent.purpose === 'verify_exists') {
return {
success: true,
exists: matchingFiles.length > 0,
message: matchingFiles.length > 0
? `Found ${matchingFiles.length} matching files`
: 'No files found',
};
}
if (matchingFiles.length === 0) {
// Generate suggestions based on the target criteria
const searchQuery = intent.target.semanticQuery || intent.target.criteria?.content || intent.target.pattern || '';
const suggestions = await this.errorRecovery.suggestForEmptySearchResults(searchQuery, 'semantic');
return {
success: false,
message: 'No files found matching target criteria',
exists: false,
suggestions,
};
}
// Return first match for read operations
const firstMatch = matchingFiles[0];
if (!firstMatch) {
return {
success: false,
message: 'No files found matching target criteria',
exists: false,
};
}
return this.handleSingleFileAccess(firstMatch, intent);
}
async updateContent(intent) {
this.logger.debug('Processing content update intent', intent);
await this.ensureIndexLoaded();
// Extract working directory from intent if provided
const workingDirectory = intent.options?.workingDirectory;
const effectiveBasePath = workingDirectory || this.basePath;
// Handle direct path access first
if (intent.target.path) {
const relativePath = this.normalizePath(intent.target.path);
const fullPath = workingDirectory ? (0, path_1.join)(effectiveBasePath, relativePath) : this.getFullPath(relativePath);
const exists = await this.fileExists(fullPath);
// Handle different update purposes
switch (intent.purpose) {
case 'create':
if (exists && !intent.options?.createPath) {
return {
success: false,
message: 'File already exists',
created: false,
};
}
break;
case 'append':
if (!exists) {
return {
success: false,
message: 'Cannot append to non-existent file',
created: false,
};
}
break;
}
// Perform the update
const result = await this.performContentUpdate(relativePath, fullPath, intent, exists);
// Update semantic index only if using default base path
if (result.success && !workingDirectory) {
this.logger.debug('Updating semantic index for file', { path: relativePath });
await this.updateFileIndex(relativePath);
await this.saveIndex();
}
this.logger.info('Content update completed', { path: relativePath, success: result.success, created: result.created });
return result;
}
const targets = await intent_processor_js_1.FileTargetProcessor.resolveTarget(intent.target, this.basePath);
const filePath = targets[0];
if (!filePath || filePath.startsWith('__')) {
return {
success: false,
message: 'Content update requires specific file path',
created: false,
};
}
const normalizedPath = this.normalizePath(filePath);
const fullPath = this.getFullPath(normalizedPath);
const exists = await this.fileExists(fullPath);
// Handle different update purposes
switch (intent.purpose) {
case 'create':
if (exists && !intent.options?.createPath) {
return {
success: false,
message: 'File already exists',
created: false,
};
}
break;
case 'append':
if (!exists) {
return {
success: false,
message: 'Cannot append to non-existent file',
created: false,
};
}
break;
}
// Perform the update
const result = await this.performContentUpdate(normalizedPath, fullPath, intent, exists);
// Update semantic index
if (result.success) {
await this.updateFileIndex(normalizedPath);
await this.saveIndex();
}
return result;
}
async organizeFiles(intent) {
await this.ensureIndexLoaded();
switch (intent.purpose) {
case 'create_directory':
return await this.createDirectory(intent);
case 'move':
return await this.moveFiles(intent);
case 'copy':
return await this.copyFiles(intent);
case 'group_semantic':
case 'group_keywords':
return await this.groupFiles(intent);
default:
return {
success: false,
filesAffected: 0,
message: `Unsupported organization purpose: ${intent.purpose}`,
};
}
}
async discoverFiles(intent) {
await this.ensureIndexLoaded();
const startTime = Date.now();
switch (intent.purpose) {
case 'list':
return await this.listFiles(intent);
case 'find':
return await this.findFiles(intent);
case 'search_content':
return await this.searchContent(intent);
case 'search_semantic':
return await this.searchSemantic(intent);
case 'search_integrated':
return await this.searchIntegrated(intent);
default:
return {
success: false,
files: [],
totalFound: 0,
searchTime: Date.now() - startTime,
message: `Unsupported discovery purpose: ${intent.purpose}`,
};
}
}
async removeFiles(intent) {
await this.ensureIndexLoaded();
const matchingFiles = await this.findFilesByTarget(intent.target);
if (matchingFiles.length === 0) {
return {
success: false,
filesDeleted: 0,
directoriesDeleted: 0,
freedSpace: 0,
deletedPaths: [],
message: 'No files found matching removal criteria',
};
}
// Handle dry run
if (intent.options?.dryRun) {
let totalSize = 0;
for (const path of matchingFiles) {
const indexEntry = this.index.entries[path];
totalSize += indexEntry?.size || 0;
}
return {
success: true,
filesDeleted: matchingFiles.length,
directoriesDeleted: 0,
freedSpace: totalSize,
deletedPaths: matchingFiles,
message: `Would delete ${matchingFiles.length} files (dry run)`,
};
}
// Perform actual deletion
return await this.performDeletion(matchingFiles, intent.options?.moveToTrash);
}
async executeWorkflow(workflow) {
const startTime = Date.now();
const stepResults = [];
let rollbackRequired = false;
const completedSteps = [];
try {
for (const step of workflow.steps) {
const stepStartTime = Date.now();
let result;
switch (step.operation) {
case 'access':
result = await this.accessFile(step.intent);
break;
case 'update':
result = await this.updateContent(step.intent);
break;
case 'organize':
result = await this.organizeFiles(step.intent);
break;
case 'discover':
result = await this.discoverFiles(step.intent);
break;
case 'remove':
result = await this.removeFiles(step.intent);
break;
default:
throw new Error(`Unknown operation: ${step.operation}`);
}
stepResults.push({
stepId: step.id,
result,
duration: Date.now() - stepStartTime,
});
if (result.success) {
completedSteps.push(step.id);
}
else if (workflow.options?.atomic) {
rollbackRequired = true;
break;
}
else if (!workflow.options?.continueOnError) {
rollbackRequired = true;
break;
}
}
// Perform rollback if needed
if (rollbackRequired && workflow.options?.atomic) {
await this.rollbackSteps(completedSteps);
}
}
catch (error) {
rollbackRequired = true;
if (workflow.options?.atomic) {
await this.rollbackSteps(completedSteps);
}
}
return {
success: !rollbackRequired,
stepResults,
totalDuration: Date.now() - startTime,
rollbackRequired,
};
}
async interpretNaturalLanguage(intent) {
const parsed = intent_processor_js_1.NaturalLanguageProcessor.parseQuery(intent.query);
return {
success: true,
interpretedIntent: parsed.intent,
confidence: parsed.confidence,
message: `Interpreted query: "${intent.query}"`,
};
}
// Private implementation methods
async ensureIndexLoaded() {
if (!this.indexLoaded) {
await this.initialize();
}
}
async loadIndex() {
try {
const indexData = await fs_1.promises.readFile(this.indexPath, 'utf8');
const loadedIndex = JSON.parse(indexData);
// Validate index version
if (loadedIndex.version !== this.indexVersion) {
console.warn('Semantic index version mismatch, rebuilding...');
await this.rebuildIndex();
return;
}
// Validate and fix keywordMap structure
if (loadedIndex.keywordMap) {
for (const keyword in loadedIndex.keywordMap) {
if (!Array.isArray(loadedIndex.keywordMap[keyword])) {
this.logger.warn(`Fixing non-array keywordMap entry for keyword: ${keyword}`);
loadedIndex.keywordMap[keyword] = [];
}
}
}
this.index = loadedIndex;
}
catch (error) {
// Index doesn't exist or is corrupted, create new one
console.log('Creating new semantic index...');
await this.rebuildIndex();
}
}
async saveIndex() {
this.index.lastUpdated = new Date().toISOString();
await fs_1.promises.writeFile(this.indexPath, JSON.stringify(this.index, null, 2));
}
async rebuildIndex() {
this.index = {
version: this.indexVersion,
created: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
entries: {},
keywordMap: {},
};
// Reset visited paths before indexing
this.visitedPaths.clear();
// Recursively index all files with depth tracking
await this.indexDirectory(this.basePath, 0);
await this.saveIndex();
}
async indexDirectory(dirPath, depth = 0) {
try {
// Check depth limit
if (depth >= this.maxIndexingDepth) {
this.logger.warn(`Skipping directory due to depth limit: ${dirPath}`);
return;
}
// Get real path to handle symlinks
const realPath = await fs_1.promises.realpath(dirPath);
// Check if we've already visited this path (handles circular symlinks)
if (this.visitedPaths.has(realPath)) {
this.logger.debug(`Skipping already visited directory: ${dirPath}`);
return;
}
// Mark as visited
this.visitedPaths.add(realPath);
const entries = await fs_1.promises.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = (0, path_1.join)(dirPath, entry.name);
const relativePath = (0, path_1.relative)(this.basePath, fullPath);
// Skip excluded directories
if (entry.isDirectory() && this.excludedDirectories.has(entry.name)) {
this.logger.debug(`Skipping excluded directory: ${entry.name}`);
continue;
}
// Skip .packfs directory
if (relativePath.startsWith('.packfs')) {
continue;
}
if (entry.isDirectory()) {
await this.indexDirectory(fullPath, depth + 1);
}
else if (entry.isFile()) {
await this.updateFileIndex(relativePath);
}
}
}
catch (error) {
this.logger.warn(`Failed to index directory ${dirPath}:`, error);
}
}
async updateIndexIfNeeded() {
// Check for new or modified files since last index update
const lastUpdate = new Date(this.index.lastUpdated);
const needsUpdate = await this.hasModificationsSince(this.basePath, lastUpdate);
if (needsUpdate) {
this.logger.info('Updating semantic index...');
// Reset visited paths before re-indexing
this.visitedPaths.clear();
await this.indexDirectory(this.basePath, 0);
await this.saveIndex();
}
}
async hasModificationsSince(dirPath, since, depth = 0) {
try {
// Prevent deep recursion
if (depth >= this.maxIndexingDepth) {
return false;
}
const entries = await fs_1.promises.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = (0, path_1.join)(dirPath, entry.name);
const relativePath = (0, path_1.relative)(this.basePath, fullPath);
// Skip excluded directories
if (entry.isDirectory() && this.excludedDirectories.has(entry.name)) {
continue;
}
if (relativePath.startsWith('.packfs'))
continue;
const stats = await fs_1.promises.stat(fullPath);
if (stats.mtime > since) {
return true;
}
if (entry.isDirectory()) {
if (await this.hasModificationsSince(fullPath, since, depth + 1)) {
return true;
}
}
}
return false;
}
catch {
return true; // Assume needs update if we can't check
}
}
async updateFileIndex(relativePath) {
const fullPath = this.getFullPath(relativePath);
try {
const stats = await fs_1.promises.stat(fullPath);
// Skip files that are too large
if (stats.size > this.maxFileSize) {
return;
}
// Skip binary files for content indexing
if (this.isBinaryFile(relativePath)) {
return;
}
const content = await fs_1.promises.readFile(fullPath, 'utf8');
const contentHash = this.calculateHash(content);
// Check if file has changed
const existingEntry = this.index.entries[relativePath];
if (existingEntry && existingEntry.contentHash === contentHash) {
return; // No changes, skip
}
// Remove old keyword mappings
if (existingEntry) {
this.removeFromKeywordMap(relativePath, existingEntry.keywords);
}
// Extract keywords and create preview
const keywords = this.extractKeywords(content);
const preview = this.generatePreview(content);
// Create index entry
const indexEntry = {
path: relativePath,
keywords,
contentHash,
lastIndexed: new Date().toISOString(),
mtime: stats.mtime.toISOString(),
size: stats.size,
mimeType: this.getMimeType(relativePath),
preview,
semanticSignature: this.generateSemanticSignature(content, keywords),
};
// Update index
this.index.entries[relativePath] = indexEntry;
this.addToKeywordMap(relativePath, keywords);
}
catch (error) {
console.warn(`Failed to index file ${relativePath}:`, error);
}
}
normalizePath(path) {
// Remove leading slash and normalize
return path.startsWith('/') ? path.substring(1) : path;
}
getFullPath(relativePath) {
return (0, path_1.join)(this.basePath, relativePath);
}
async fileExists(fullPath) {
try {
await fs_1.promises.access(fullPath);
return true;
}
catch {
return false;
}
}
isBinaryFile(filePath) {
const ext = (0, path_1.extname)(filePath).toLowerCase();
const binaryExts = [
'.jpg',
'.jpeg',
'.png',
'.gif',
'.pdf',
'.zip',
'.tar',
'.gz',
'.exe',
'.bin',
];
return binaryExts.includes(ext);
}
calculateHash(content) {
// Simple hash function - in production, use crypto.createHash
let hash = 0;
for (let i = 0; i < content.length; i++) {
const char = content.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(36);
}
extractKeywords(content) {
// Enhanced keyword extraction
const words = content
.toLowerCase()
.replace(/[^\w\s]/g, ' ')
.split(/\s+/)
.filter((word) => word.length > 3)
.filter((word) => !this.isStopWord(word));
// Get word frequency
const frequency = {};
for (const word of words) {
frequency[word] = (frequency[word] || 0) + 1;
}
// Return top keywords by frequency
return Object.entries(frequency)
.sort(([, a], [, b]) => b - a)
.slice(0, 15)
.map(([word]) => word);
}
isStopWord(word) {
const stopWords = new Set([
'the',
'and',
'or',
'but',
'in',
'on',
'at',
'to',
'for',
'of',
'with',
'by',
'is',
'are',
'was',
'were',
'will',
'would',
'could',
'should',
'have',
'has',
'had',
'this',
'that',
'these',
'those',
'not',
'from',
'into',
'through',
'during',
'before',
'after',
'above',
'below',
'between',
'among',
]);
return stopWords.has(word);
}
generatePreview(content) {
// Generate intelligent preview
const lines = content.split('\n');
const meaningfulLines = lines.filter((line) => line.trim().length > 10);
return meaningfulLines.slice(0, 3).join('\n').substring(0, 300);
}
getMimeType(filePath) {
const ext = (0, path_1.extname)(filePath).toLowerCase();
const mimeTypes = {
'.txt': 'text/plain',
'.md': 'text/markdown',
'.js': 'application/javascript',
'.ts': 'application/typescript',
'.json': 'application/json',
'.html': 'text/html',
'.css': 'text/css',
'.py': 'text/x-python',
'.java': 'text/x-java-source',
'.cpp': 'text/x-c++src',
'.c': 'text/x-csrc',
};
return mimeTypes[ext] || 'text/plain';
}
generateSemanticSignature(_content, keywords) {
// Generate a semantic signature for similarity comparison
// This is a simplified version - production would use embeddings
return keywords.slice(0, 5).sort().join('|');
}
addToKeywordMap(filePath, keywords) {
for (const keyword of keywords) {
if (!this.index.keywordMap[keyword]) {
this.index.keywordMap[keyword] = [];
}
// Defensive check to ensure it's an array
if (!Array.isArray(this.index.keywordMap[keyword])) {
this.logger.warn(`Converting non-array keywordMap entry to array for keyword: ${keyword}`);
this.index.keywordMap[keyword] = [];
}
if (!this.index.keywordMap[keyword].includes(filePath)) {
this.index.keywordMap[keyword].push(filePath);
}
}
}
removeFromKeywordMap(filePath, keywords) {
for (const keyword of keywords) {
if (this.index.keywordMap[keyword]) {
// Defensive check to ensure it's an array
if (!Array.isArray(this.index.keywordMap[keyword])) {
this.logger.warn(`Skipping removal from non-array keywordMap entry for keyword: ${keyword}`);
delete this.index.keywordMap[keyword];
continue;
}
const index = this.index.keywordMap[keyword].indexOf(filePath);
if (index > -1) {
this.index.keywordMap[keyword].splice(index, 1);
if (this.index.keywordMap[keyword].length === 0) {
delete this.index.keywordMap[keyword];
}
}
}
}
}
async handleSingleFileAccessWithBasePath(relativePath, intent, basePath) {
const fullPath = (0, path_1.join)(basePath, relativePath);
const exists = await this.fileExists(fullPath);
if (!exists) {
if (intent.purpose === 'create_or_get') {
// Create empty file
const dir = (0, path_1.dirname)(fullPath);
await fs_1.promises.mkdir(dir, { recursive: true });
await fs_1.promises.writeFile(fullPath, '');
// Note: Skip index update for runtime base path operations
const metadata = await this.getFileMetadataWithBasePath(relativePath, basePath);
return {
success: true,
content: '',
metadata,
exists: true,
};
}
// For verify_exists, success means the operation worked, exists indicates file presence
if (intent.purpose === 'verify_exists') {
const suggestions = await this.errorRecovery.suggestForFileNotFound(relativePath);
return {
success: true,
exists: false,
message: `File not found: ${relativePath}`,
suggestions,
};
}
// Generate helpful suggestions for file not found
const suggestions = await this.errorRecovery.suggestForFileNotFound(relativePath);
const context = {
operation: 'accessFile',
requestedPath: relativePath,
error: `File not found: ${relativePath}`,
suggestions,
};
return {
success: false,
exists: false,
message: this.errorRecovery.formatSuggestions(context),
suggestions,
};
}
// Handle different access purposes
switch (intent.purpose) {
case 'read':
const content = await fs_1.promises.readFile(fullPath, intent.preferences?.encoding || 'utf8');
return {
success: true,
content,
metadata: intent.preferences?.includeMetadata
? await this.getFileMetadataWithBasePath(relativePath, basePath)
: undefined,
exists: true,
};
case 'preview':
return {
success: true,
preview: await this.generateFilePreview(fullPath),
metadata: await this.getFileMetadataWithBasePath(relativePath, basePath),
exists: true,
};
case 'metadata':
return {
success: true,
metadata: await this.getFileMetadataWithBasePath(relativePath, basePath),
exists: true,
};
case 'verify_exists':
return {
success: true,
exists: true,
};
case 'create_or_get':
const existingContent = await fs_1.promises.readFile(fullPath, intent.preferences?.encoding || 'utf8');
return {
success: true,
content: existingContent,
metadata: await this.getFileMetadataWithBasePath(relativePath, basePath),
exists: true,
};
default:
return {
success: false,
exists: true,
message: `Unsupported access purpose: ${intent.purpose}`,
};
}
}
async handleSingleFileAccess(relativePath, intent) {
const fullPath = this.getFullPath(relativePath);
const exists = await this.fileExists(fullPath);
if (!exists) {
if (intent.purpose === 'create_or_get') {
// Create empty file
const dir = (0, path_1.dirname)(fullPath);
await fs_1.promises.mkdir(dir, { recursive: true });
await fs_1.promises.writeFile(fullPath, '');
await this.updateFileIndex(relativePath);
await this.saveIndex();
const metadata = await this.getFileMetadata(relativePath);
return {
success: true,
content: '',
metadata,
exists: true,
};
}
// For verify_exists, success means the operation worked, exists indicates file presence
if (intent.purpose === 'verify_exists') {
const suggestions = await this.errorRecovery.suggestForFileNotFound(relativePath);
return {
success: true,
exists: false,
message: `File not found: ${relativePath}`,
suggestions,
};
}
// Generate helpful suggestions for file not found
const suggestions = await this.errorRecovery.suggestForFileNotFound(relativePath);
const context = {
operation: 'accessFile',
requestedPath: relativePath,
error: `File not found: ${relativePath}`,
suggestions,
};
return {
success: false,
exists: false,
message: this.errorRecovery.formatSuggestions(context),
suggestions,
};
}
// Handle different access purposes
switch (intent.purpose) {
case 'read':
const content = await fs_1.promises.readFile(fullPath, intent.preferences?.encoding || 'utf8');
return {
success: true,
content,
metadata: intent.preferences?.includeMetadata
? await this.getFileMetadata(relativePath)
: undefined,
exists: true,
};
case 'preview':
const indexEntry = this.index.entries[relativePath];
return {
success: true,
preview: indexEntry?.preview || (await this.generateFilePreview(fullPath)),
metadata: await this.getFileMetadata(relativePath),
exists: true,
};
case 'metadata':
return {
success: true,
metadata: await this.getFileMetadata(relativePath),
exists: true,
};
case 'verify_exists':
return {
success: true,
exists: true,
};
case 'create_or_get':
const existingContent = await fs_1.promises.readFile(fullPath, intent.preferences?.encoding || 'utf8');
return {
success: true,
content: existingContent,
metadata: await this.getFileMetadata(relativePath),
exists: true,
};
default:
return {
success: false,
exists: true,
message: `Unsupported access purpose: ${intent.purpose}`,
};
}
}
async getFileMetadata(relativePath) {
const fullPath = this.getFullPath(relativePath);
const stats = await fs_1.promises.stat(fullPath);
const indexEntry = this.index.entries[relativePath];
return {
path: relativePath,
size: stats.size,
mtime: stats.mtime,
isDirectory: stats.isDirectory(),
permissions: stats.mode,
mimeType: indexEntry?.mimeType || this.getMimeType(relativePath),
tags: indexEntry?.keywords,
semanticSignature: indexEntry?.semanticSignature,
};
}
async getFileMetadataWithBasePath(relativePath, basePath) {
const fullPath = (0, path_1.join)(basePath, relativePath);
const stats = await fs_1.promises.stat(fullPath);
return {
path: relativePath,
size: stats.size,
mtime: stats.mtime,
isDirectory: stats.isDirectory(),
permissions: stats.mode,
mimeType: this.getMimeType(relativePath),
tags: undefined, // No index lookup for runtime base path
semanticSignature: undefined, // No index lookup for runtime base path
};
}
async generateFilePreview(fullPath) {
try {
const content = await fs_1.promises.readFile(fullPath, 'utf8');
return this.generatePreview(content);
}
catch {
return 'Preview unavailable';
}
}
async performContentUpdate(_relativePath, fullPath, intent, exists) {
const contentBuffer = typeof intent.content === 'string' ? Buffer.from(intent.content, 'utf8') : intent.content;
let newContent;
let created = false;
try {
switch (intent.purpose) {
case 'create':
case 'overwrite':
newContent = contentBuffer;
created = !exists;
break;
case 'append':
if (!exists) {
throw new Error('Cannot append to non-existent file');
}
const existingContent = await fs_1.promises.readFile(fullPath);
newContent = Buffer.concat([existingContent, contentBuffer]);
break;
case 'merge':
if (exists) {
const existingContent = await fs_1.promises.readFile(fullPath, 'utf8');
const newContentStr = contentBuffer.toString('utf8');
newContent = Buffer.from(`${existingContent}\n${newContentStr}`, 'utf8');
}
else {
newContent = contentBuffer;
created = true;
}
break;
case 'patch':
// Simplified patch - real implementation would handle diffs
newContent = contentBuffer;
break;
default:
throw new Error(`Unsupported update purpose: ${intent.purpose}`);
}
// Ensure directory exists
const dir = (0, path_1.dirname)(fullPath);
await fs_1.promises.mkdir(dir, { recursive: true });
// Write the file
await fs_1.promises.writeFile(fullPath, newContent);
return {
success: true,
bytesWritten: newContent.length,
created,
};
}
catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error',
created: false,
};
}
}
async findFilesByTarget(target) {
if (target.path) {
const relativePath = this.normalizePath(target.path);
return (await this.fileExists(this.getFullPath(relativePath))) ? [relativePath] : [];
}
let results = [];
if (target.pattern) {
results = results.concat(this.findByPattern(target.pattern));
}
if (target.semanticQuery) {
results = results.concat(this.findBySemanticQuery(target.semanticQuery));
}
if (target.criteria) {
results = results.concat(this.findByCriteria(target.criteria));
}
// Remove duplicates and return
return [...new Set(results)];
}
findBySemanticQuery(query) {
const queryWords = query.toLowerCase().split(/\s+/);
const matches = [];
for (const [path, entry] of Object.entries(this.index.entries)) {
let score = 0;
// Score based on keyword matches
for (const keyword of entry.keywords) {
for (const queryWord of queryWords) {
if (keyword.includes(queryWord)) {
score += 2;
}
}
}
// Score based on filename matches (higher weight)
const filename = (0, path_1.basename)(path).toLowerCase();
for (const queryWord of queryWords) {
if (filename.includes(queryWord)) {
score += 3;
}
}
// Handle common file types with special scoring
if (query.toLowerCase().includes('readme') && filename.includes('readme')) {
score += 10;
}
if (query.toLowerCase().includes('config') && filename.includes('config')) {
score += 10;
}
// Score based on content matches (if available)
if (entry.preview) {
for (const queryWord of queryWords) {
if (entry.preview.toLowerCase().includes(queryWord)) {
score += 1;
}
}
}
if (score > 0) {
matches.push({ path, score });
}
}
return matches
.sort((a, b) => b.score - a.score)
.slice(0, this.config.defaultMaxResults)
.map((m) => m.path);
}
findByCriteria(criteria) {
const matches = [];
for (const [path, entry] of Object.entries(this.index.entries)) {
let isMatch = true;
if (criteria.name && !path.includes(criteria.name)) {
isMatch = false;
}
if (criteria.content) {
// Search in keywords and preview
const searchTerm = criteria.content.toLowerCase();
const hasKeywordMatch = entry.keywords.some((k) => k.includes(searchTerm));
const hasPreviewMatch = entry.preview.toLowerCase().includes(searchTerm);
if (!hasKeywordMatch && !hasPreviewMatch) {
isMatch = false;
}
}
if (criteria.size) {
if (criteria.size.min && entry.size < criteria.size.min) {
isMatch = false;
}
if (criteria.size.max && entry.size > criteria.size.max) {
isMatch = false;
}
}
if (criteria.modified) {
const mtime = new Date(entry.mtime);
if (criteria.modified.after && mtime < criteria.modified.after) {
isMatch = false;
}
if (criteria.modified.before && mtime > criteria.modified.before) {
isMatch = false;
}
}
if (criteria.type) {
const ext = (0, path_1.extname)(path).substring(1);
if (!criteria.type.includes(ext)) {
isMatch = false;
}
}
if (isMatch) {
matches.push(path);
}
}
return matches;
}
findByPattern(pattern) {
try {
// Handle common patterns
if (pattern === '*' || pattern === '**' || pattern === '*.*') {
return Object.keys(this.index.entries);
}
// Simple glob pattern matching with safety checks
let regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.');
// Ensure pattern is valid
if (regexPattern === '.*') {
return Object.keys(this.index.entries);
}
const regex = new RegExp(regexPattern, 'i');
return Object.keys(this.index.entries).filter((path) => regex.test(path));
}
catch (error) {
console.warn(`Invalid pattern '${pattern}':`, error);
// Fallback to simple contains match
return Object.keys(this.index.entries).filter((path) => path.toLowerCase().includes(pattern.toLowerCase().replace(/\*/g, '')));
}
}
// Additional implementation methods continue...
// (createDirectory, moveFiles, copyFiles, groupFiles, listFiles, findFiles,
// searchContent, searchSemantic, searchIntegrated, performDeletion, rollbackSteps)
async createDirectory(intent) {
if (!intent.destination?.path) {
return {
success: false,
filesAffected: 0,
message: 'Create directory requires destination path',
};
}
const relativePath = this.normalizePath(intent.destination.path);
const fullPath = this.getFullPath(relativePath);
try {
await fs_1.promises.mkdir(fullPath, { recursive: intent.options?.recursive });
return {
success: true,
filesAffected: 1,
message: `Directory created: ${relativePath}`,
};
}
catch (error) {
return {
success: false,
filesAffected: 0,
message: `Failed to create directory: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
}
async moveFiles(intent) {
if (!intent.source || !intent.destination?.path) {
return {
success: false,
filesAffected: 0,
message: 'Move operation requires source and destination paths',
};
}
const sourceFiles = await this.findFilesByTarget(intent.source);
const newPaths = [];
try {
for (const sourcePath of sourceFiles) {
const sourceFullPath = this.getFullPath(sourcePath);
const destPath = this.normalizePath(intent.destination.path);
const destFullPath = this.getFullPath(destPath);
// Ensure destination directory exists
await fs_1.promises.mkdir((0, path_1.dirname)(destFullPath), { recursive: true });
// Move the file
await fs_1.promises.rename(sourceFullPath, destFullPath);
// Update index
const indexEntry = this.index.entries[sourcePath];
if (indexEntry) {
delete this.index.entries[sourcePath];
this.removeFromKeywordMap(sourcePath, indexEntry.keywords);
indexEntry.path = destPath;
this.index.entries[destPath] = indexEntry;
this.addToKeywordMap(destPath, indexEntry.keywords);
}
newPaths.push(destPath);
}
await this.saveIndex();
return {
success: true,
filesAffected: newPaths.length,
newPaths,
};
}
catch (error) {
return {
success: false,
filesAffected: 0,
message: `Failed to move files: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
}
async copyFiles(intent) {
if (!intent.source || !intent.destination?.path) {
return {
success: false,
filesAffected: 0,
message: 'Copy operation requires source and destination paths',
};
}
const sourceFiles = await this.findFilesByTarget(intent.source);
const newPaths = [];
try {
for (const sourcePath of sourceFiles) {
const sourceFullPath = this.getFullPath(sourcePath);
const destPath = this.normalizePath(intent.destination.path);
const destFullPath = this.getFullPath(destPath);
// Ensure destination directory exists
await fs_1.promises.mkdir((0, path_1.dirname)(destFullPath), { recursive: true });
// Copy the file
await fs_1.promises.copyFile(sourceFullPath, destFullPath);
// Update index for new file
await this.updateFileIndex(destPath);
newPaths.push(destPath);
}
await this.saveIndex();
return {
success: true,
filesAffected: newPaths.length,
newPaths,
};
}
catch (error) {
return {
success: false,
filesAffected: 0,
message: `Failed to copy files: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
}
async groupFiles(intent) {
const allFiles = Object.keys(this.index.entries);
const groups = new Map();
if (intent.purpose === 'group_keywords') {
// Group by common keywords
for (const path of allFiles) {
const entry = this.index.entries[path];
if (!entry)
continue;
for (const keyword of entry.keywords) {
if (!groups.has(keyword)) {
groups.set(keyword, []);
}
groups.get(keyword).push(path);
}
}
}
else {
// Simplified semantic grouping by semantic signature
const signatureGroups = new Map();
for (const path of allFiles) {
const entry = this.index.entries[path];
if (!entry)
continue;