packfs-core
Version:
Semantic filesystem operations for LLM agent frameworks with natural language understanding. See LLM_AGENT_GUIDE.md for copy-paste examples.
826 lines • 40.6 kB
JavaScript
/**
* Mastra framework integration for PackFS semantic filesystem
* Mastra: The TypeScript AI framework - https://mastra.ai/
*/
import { DiskSemanticBackend } from '../semantic/disk-semantic-backend.js';
/**
* PackFS semantic filesystem tool for Mastra
* Provides unified file operations through natural language interface
*/
export class MastraSemanticFilesystemTool {
createTool(config) {
return {
name: 'semantic_filesystem',
description: this.getToolDescription().description,
parameters: this.getToolDescription().parameters,
execute: async (params) => {
try {
// Validate parameters
const validation = this.validateParameters(params);
if (!validation.valid) {
return {
success: false,
error: `Invalid parameters: ${validation.errors?.join(', ')}`,
};
}
const startTime = Date.now();
let result;
const filesAccessed = [];
// Handle different operation types through natural language
if (params.naturalLanguageQuery) {
result = await this.executeNaturalLanguageQuery(config, params.naturalLanguageQuery);
}
else {
result = await this.executeStructuredOperation(config, params);
}
// Track files accessed for metadata
if (params.target?.path) {
filesAccessed.push(params.target.path);
}
// Check if the semantic operation was successful
const success = result.success !== false;
// Flatten the result structure for LLM compatibility
// LLMs expect direct access to properties like content, exists, files, etc.
const flatResult = {
success,
error: success ? undefined : result.message || 'Operation failed',
// Spread the result object to flatten nested properties
...(success && result ? result : {}),
// Preserve metadata but enhance it with execution info
metadata: {
...(result?.metadata || {}),
executionTime: Date.now() - startTime,
filesAccessed,
operationType: params.operation || 'natural_language',
},
};
// Remove any duplicate 'success' property from spreading
if ('success' in flatResult && flatResult.success === success) {
delete flatResult.success;
flatResult.success = success;
}
return flatResult;
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
},
};
}
getToolDescription() {
return {
name: 'semantic_filesystem',
description: 'Perform intelligent file operations using semantic understanding. Supports natural language queries like "create a config file", "find all documentation", or "read the main script file".\n\n' +
'IMPORTANT: Use the workingDirectory parameter to operate on different project directories. For example:\n' +
'- To read from a specific project: {"operation": "access", "purpose": "read", "target": {"path": "README.md"}, "workingDirectory": "/path/to/project"}\n' +
'- To search in a context network: {"operation": "discover", "purpose": "search_semantic", "target": {"query": "configuration"}, "workingDirectory": "/path/to/context-network"}',
parameters: {
type: 'object',
properties: {
naturalLanguageQuery: {
type: 'string',
description: 'Natural language description of the file operation to perform (e.g., "create a file called notes.txt with my thoughts", "find all JavaScript files", "read the README file")',
},
workingDirectory: {
type: 'string',
description: 'IMPORTANT: Specify the project directory or context network path to operate on. This allows you to work with different projects without reinitializing the tool. Use absolute paths (e.g., "/projects/my-project/context-network"). This parameter should be used whenever you need to access files in a specific project directory.',
},
operation: {
type: 'string',
description: 'Structured operation type when not using natural language',
enum: ['access', 'update', 'discover', 'organize', 'remove'],
},
purpose: {
type: 'string',
description: 'Specific purpose within the operation type',
enum: [
// Access purposes
'read',
'preview',
'metadata',
'verify_exists',
'create_or_get',
// Update purposes
'create',
'append',
'overwrite',
'merge',
'patch',
// Discovery purposes
'list',
'find',
'search_content',
'search_semantic',
'search_integrated',
// Organization purposes
'create_directory',
'move',
'copy',
'group_semantic',
'group_keywords',
// Removal purposes
'delete_file',
'delete_directory',
'delete_by_criteria',
],
},
target: {
type: 'object',
description: 'How to identify the target file(s)',
},
content: {
type: 'string',
description: 'Content to write when creating/updating files',
},
destination: {
type: 'object',
description: 'Destination for move/copy operations',
},
options: {
type: 'object',
description: 'Additional operation options',
},
},
required: [],
},
examples: [
{
input: '{"naturalLanguageQuery": "create a file called todo.txt with my daily tasks"}',
description: 'Create a new file using natural language',
},
{
input: '{"naturalLanguageQuery": "find all TypeScript files in the src directory"}',
description: 'Search for files using natural language',
},
{
input: '{"naturalLanguageQuery": "read the package.json file"}',
description: 'Access file content using natural language',
},
{
input: '{"operation": "discover", "purpose": "search_semantic", "target": {"semanticQuery": "configuration"}}',
description: 'Structured semantic search for configuration files',
},
{
input: '{"operation": "access", "purpose": "read", "target": {"path": "README.md"}}',
description: 'Structured file reading operation',
},
{
input: '{"operation": "access", "purpose": "read", "target": {"path": "context-network/discovery.md"}, "workingDirectory": "/projects/project-a"}',
description: 'Read file from a specific project directory',
},
],
};
}
validateParameters(params) {
const errors = [];
// Must have either natural language query or structured operation
if (!params.naturalLanguageQuery && !params.operation) {
errors.push('Must provide either naturalLanguageQuery or operation');
}
// If using structured operation, validate required fields
if (params.operation) {
if (!params.purpose) {
errors.push('Purpose is required when using structured operations');
}
if (!params.target && !['create_directory'].includes(params.purpose)) {
errors.push('Target is required for most operations');
}
if (['create', 'append', 'overwrite', 'merge', 'patch'].includes(params.purpose) &&
!params.content &&
!params.naturalLanguageQuery) {
errors.push('Content is required for content update operations');
}
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
};
}
async executeNaturalLanguageQuery(config, query) {
// Ensure filesystem is initialized
if (!config.filesystem) {
throw new Error('PackFS Error: Filesystem is not initialized. ' +
'You must provide either:\n' +
'1. workingDirectory: an absolute path to your project directory\n' +
' Example: { workingDirectory: "/path/to/project" }\n' +
'2. filesystem: a pre-initialized filesystem instance\n' +
' Example: { filesystem: new DiskSemanticBackend("/path/to/project") }\n\n' +
'For more details, see: https://github.com/jwynia/PackFS/blob/main/docs/GETTING_STARTED.md');
}
// Use the semantic filesystem's natural language processing
const nlResult = await config.filesystem.interpretNaturalLanguage({
query,
context: {
workingDirectory: config.workingDirectory,
agentContext: config.mastra?.agentContext,
},
});
if (!nlResult.success) {
throw new Error(`Failed to interpret query: ${nlResult.message}`);
}
// Execute the interpreted intent
const intent = nlResult.interpretedIntent;
if ('purpose' in intent) {
switch (intent.purpose) {
case 'read':
case 'preview':
case 'metadata':
case 'verify_exists':
case 'create_or_get':
return await config.filesystem.accessFile(intent);
case 'create':
case 'append':
case 'overwrite':
case 'merge':
case 'patch':
return await config.filesystem.updateContent(intent);
case 'create_directory':
case 'move':
case 'copy':
case 'group_semantic':
case 'group_keywords':
return await config.filesystem.organizeFiles(intent);
case 'list':
case 'find':
case 'search_content':
case 'search_semantic':
case 'search_integrated':
return await config.filesystem.discoverFiles(intent);
case 'delete_file':
case 'delete_directory':
case 'delete_by_criteria':
return await config.filesystem.removeFiles(intent);
}
}
// Handle workflow intents
return await config.filesystem.executeWorkflow(intent);
}
async executeStructuredOperation(config, params) {
// Ensure filesystem is initialized
if (!config.filesystem) {
throw new Error('PackFS Error: Filesystem is not initialized. ' +
'You must provide either:\n' +
'1. workingDirectory: an absolute path to your project directory\n' +
' Example: { workingDirectory: "/path/to/project" }\n' +
'2. filesystem: a pre-initialized filesystem instance\n' +
' Example: { filesystem: new DiskSemanticBackend("/path/to/project") }\n\n' +
'For more details, see: https://github.com/jwynia/PackFS/blob/main/docs/GETTING_STARTED.md');
}
// Merge workingDirectory into options if provided
const operationOptions = {
...params.options,
...(params.workingDirectory && { workingDirectory: params.workingDirectory })
};
switch (params.operation) {
case 'access':
return await config.filesystem.accessFile({
purpose: params.purpose,
target: params.target,
preferences: params.options,
options: operationOptions,
});
case 'update':
return await config.filesystem.updateContent({
purpose: params.purpose,
target: params.target,
content: params.content,
options: operationOptions,
});
case 'discover':
return await config.filesystem.discoverFiles({
purpose: params.purpose,
target: params.target,
options: operationOptions,
});
case 'organize':
return await config.filesystem.organizeFiles({
purpose: params.purpose,
source: params.source,
destination: params.destination,
options: operationOptions,
});
case 'remove':
return await config.filesystem.removeFiles({
purpose: params.purpose,
target: params.target,
options: operationOptions,
});
default:
throw new Error(`Unknown operation: ${params.operation}`);
}
}
}
/**
* Create a Mastra semantic filesystem tool with the given configuration
*/
export function createMastraSemanticFilesystemTool(config) {
const adapter = new MastraSemanticFilesystemTool();
return adapter.createTool(config);
}
/**
* Utility to create multiple Mastra tools for different semantic operations
* Provides more granular control for complex agent workflows
*/
export function createMastraSemanticToolSuite(config) {
// Initialize configuration with defaults and handle backward compatibility
const normalizedConfig = {
// Handle both rootPath and basePath for backward compatibility
workingDirectory: config.workingDirectory || config.rootPath || config.basePath,
// Initialize filesystem if not provided
filesystem: config.filesystem || createDefaultFilesystem(config),
// Copy other properties
security: config.security,
performance: config.performance,
mastra: config.mastra,
};
// Validate configuration
if (!normalizedConfig.workingDirectory) {
throw new Error('PackFS Error: rootPath or basePath is required for Mastra integration');
}
if (!normalizedConfig.filesystem) {
throw new Error('PackFS Error: Could not initialize filesystem. Please provide a valid filesystem object or rootPath.');
}
const baseAdapter = new MastraSemanticFilesystemTool();
return {
fileReader: {
name: 'read_file',
description: 'Read and access file content with semantic understanding',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Natural language description of what file to read (e.g., "read the main configuration file", "show me the README")',
},
path: {
type: 'string',
description: 'Direct file path if known',
},
purpose: {
type: 'string',
enum: ['read', 'preview', 'metadata'],
description: 'How to access the file',
},
context: {
type: 'object',
description: 'Context object for backward compatibility',
},
},
required: [],
},
execute: async (params) => {
try {
// Handle both direct parameters and wrapped context
const inputParams = params.context || params;
if (inputParams.query) {
const result = await baseAdapter.createTool(normalizedConfig).execute({
naturalLanguageQuery: inputParams.query,
});
// The base adapter already flattens, but keep consistent with other methods
return result;
}
else {
// Handle different parameter formats
const purpose = inputParams.purpose || 'read';
const path = inputParams.path || (inputParams.target && inputParams.target.path);
if (!path) {
return {
success: false,
error: 'Path is required for file operations',
troubleshooting: {
expectedFormat: 'Either provide path directly or target.path in context object',
example: '{ path: "file.txt", purpose: "read" } or { context: { purpose: "read", target: { path: "file.txt" } } }',
},
};
}
const result = await baseAdapter.createTool(normalizedConfig).execute({
operation: 'access',
purpose: purpose,
target: { path: path },
preferences: inputParams.preferences || inputParams.options,
});
// Ensure consistent response structure and include content field
if (result.success) {
return {
success: true,
exists: result.exists,
content: result.content,
metadata: result.metadata,
executionMetadata: result.metadata,
};
}
return result;
}
}
catch (error) {
return {
success: false,
error: error instanceof Error ? `PackFS Error: ${error.message}` : 'Unknown PackFS error',
troubleshooting: {
checkConfiguration: true,
suggestedFix: 'Ensure filesystem object is properly initialized and path is valid',
},
};
}
},
},
fileWriter: {
name: 'write_file',
description: 'Create and modify files with intelligent content handling',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Natural language description of what to write (e.g., "create a README with project info", "add my notes to the todo file")',
},
path: {
type: 'string',
description: 'File path to write to',
},
content: {
type: 'string',
description: 'Content to write',
},
mode: {
type: 'string',
enum: ['create', 'append', 'overwrite'],
description: 'How to write the content',
},
context: {
type: 'object',
description: 'Context object for backward compatibility',
},
},
required: [],
},
execute: async (params) => {
try {
// Handle both direct parameters and wrapped context
const inputParams = params.context || params;
if (inputParams.query) {
const result = await baseAdapter.createTool(normalizedConfig).execute({
naturalLanguageQuery: inputParams.query,
});
// The base adapter already flattens, but keep consistent with other methods
return result;
}
else {
// Handle different parameter formats
const purpose = inputParams.purpose || inputParams.mode || 'create';
const path = inputParams.path || (inputParams.target && inputParams.target.path);
const content = inputParams.content;
if (!path) {
return {
success: false,
error: 'Path is required for file operations',
troubleshooting: {
expectedFormat: 'Either provide path directly or target.path in context object',
example: '{ path: "file.txt", content: "data", mode: "create" } or { context: { purpose: "create", target: { path: "file.txt" }, content: "data" } }',
},
};
}
if (!content && ['create', 'update', 'append'].includes(purpose)) {
return {
success: false,
error: 'Content is required for write operations',
troubleshooting: {
expectedFormat: 'Provide content parameter with the data to write',
example: '{ path: "file.txt", content: "data", mode: "create" }',
},
};
}
const result = await baseAdapter.createTool(normalizedConfig).execute({
operation: 'update',
purpose: purpose,
target: { path: path },
content: content,
options: inputParams.options,
});
// Ensure consistent response structure
if (result.success) {
// Result is already flattened by base adapter
return result;
}
return result;
}
}
catch (error) {
return {
success: false,
error: error instanceof Error ? `PackFS Error: ${error.message}` : 'Unknown PackFS error',
troubleshooting: {
checkConfiguration: true,
suggestedFix: 'Ensure filesystem object is properly initialized and parameters are valid',
},
};
}
},
},
fileSearcher: {
name: 'search_files',
description: 'Find files using semantic search and natural language queries',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Natural language search query (e.g., "find all JavaScript files", "look for configuration files", "search for files about machine learning")',
},
pattern: {
type: 'string',
description: 'Glob pattern for file matching',
},
maxResults: {
type: 'number',
description: 'Maximum number of results to return',
},
context: {
type: 'object',
description: 'Context object for backward compatibility',
},
},
required: [],
},
execute: async (params) => {
try {
// Handle both direct parameters and wrapped context
const inputParams = params.context || params;
if (inputParams.query) {
const result = await baseAdapter.createTool(normalizedConfig).execute({
naturalLanguageQuery: inputParams.query,
});
// The base adapter already flattens, so check top-level properties
if (result.success) {
return {
success: true,
results: result.files || result.results || [],
totalFound: result.totalFound || 0,
searchTime: result.searchTime || 0,
metadata: result.metadata,
};
}
return result;
}
else {
// Handle different parameter formats
const purpose = inputParams.purpose || 'search_content';
const path = inputParams.path || (inputParams.target && inputParams.target.path) || '.';
const query = inputParams.searchQuery || inputParams.pattern || '';
const result = await baseAdapter.createTool(normalizedConfig).execute({
operation: 'discover',
purpose: purpose,
target: {
path: path,
query: query,
criteria: inputParams.criteria,
},
options: inputParams.options || {
maxResults: inputParams.maxResults,
recursive: true,
},
});
// Fix response structure: convert 'files' to 'results' and ensure proper structure
if (result.success) {
return {
success: true,
results: result.files || result.results || [],
totalFound: result.totalFound || 0,
searchTime: result.searchTime || 0,
metadata: result.metadata,
};
}
return result;
}
}
catch (error) {
return {
success: false,
error: error instanceof Error ? `PackFS Error: ${error.message}` : 'Unknown PackFS error',
troubleshooting: {
checkConfiguration: true,
suggestedFix: 'Ensure filesystem object is properly initialized and parameters are valid',
},
};
}
},
},
fileOrganizer: {
name: 'organize_files',
description: 'Move, copy, and organize files intelligently',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Natural language description of organization task (e.g., "move all images to photos folder", "copy config files to backup")',
},
operation: {
type: 'string',
enum: ['move', 'copy', 'create_directory', 'list'],
description: 'Organization operation to perform',
},
source: {
type: 'string',
description: 'Source file or pattern',
},
destination: {
type: 'string',
description: 'Destination path',
},
context: {
type: 'object',
description: 'Context object for backward compatibility',
},
},
required: [],
},
execute: async (params) => {
try {
// Handle both direct parameters and wrapped context
const inputParams = params.context || params;
if (inputParams.query) {
const result = await baseAdapter.createTool(normalizedConfig).execute({
naturalLanguageQuery: inputParams.query,
});
// The base adapter already flattens, but keep consistent with other methods
return result;
}
else {
// Special handling for list operation which is actually a discover operation
if (inputParams.purpose === 'list' || inputParams.operation === 'list') {
// Use source parameter if provided, otherwise fall back to path or target.path
const path = inputParams.source ||
inputParams.path ||
(inputParams.target && inputParams.target.path) ||
'.';
const result = await baseAdapter.createTool(normalizedConfig).execute({
operation: 'discover',
purpose: 'list',
target: { path: path },
options: inputParams.options,
});
// Fix response structure: convert 'files' to 'results' and ensure proper structure
if (result.success) {
return {
success: true,
results: result.files || result.results || [],
totalFound: result.totalFound || 0,
searchTime: result.searchTime || 0,
metadata: result.metadata,
};
}
return result;
}
// Handle different parameter formats for other operations
const purpose = inputParams.purpose || inputParams.operation || 'move';
const sourcePath = inputParams.source || (inputParams.target && inputParams.target.path);
const destPath = inputParams.destination || inputParams.dest;
if (!sourcePath && purpose !== 'create_directory') {
return {
success: false,
error: 'Source path is required for organize operations',
troubleshooting: {
expectedFormat: 'Provide source parameter with the file/directory to organize',
example: '{ operation: "move", source: "file.txt", destination: "folder/" }',
},
};
}
if (!destPath) {
return {
success: false,
error: 'Destination path is required for organize operations',
troubleshooting: {
expectedFormat: 'Provide destination parameter with the target location',
example: '{ operation: "move", source: "file.txt", destination: "folder/" }',
},
};
}
// For create_directory operation, we don't need a source
if (purpose === 'create_directory') {
const result = await baseAdapter.createTool(normalizedConfig).execute({
operation: 'organize',
purpose: purpose,
destination: { path: destPath },
options: inputParams.options,
});
// Ensure consistent response structure
if (result.success && result.data) {
return {
success: true,
...result.data,
metadata: result.metadata,
};
}
return result;
}
// For other operations like copy and move, we need both source and destination
try {
const result = await baseAdapter.createTool(normalizedConfig).execute({
operation: 'organize',
purpose: purpose,
source: { path: sourcePath },
destination: { path: destPath },
options: inputParams.options,
});
// If operation failed, provide more detailed error information
if (!result.success) {
console.log(`File organization operation failed: ${purpose}`, {
source: sourcePath,
destination: destPath,
error: result.message || result.error || 'Unknown error',
});
return {
success: false,
error: `Failed to ${purpose} file: ${result.message || result.error || 'Unknown error'}`,
troubleshooting: {
operation: purpose,
source: sourcePath,
destination: destPath,
suggestedFix: 'Check file paths and permissions',
},
originalError: result,
};
}
// Ensure consistent response structure
if (result.success) {
// Result is already flattened by base adapter
return result;
}
return result;
}
catch (error) {
console.error(`Exception in ${purpose} operation:`, error);
throw error;
}
}
}
catch (error) {
return {
success: false,
error: error instanceof Error ? `PackFS Error: ${error.message}` : 'Unknown PackFS error',
troubleshooting: {
checkConfiguration: true,
suggestedFix: 'Ensure filesystem object is properly initialized and parameters are valid',
},
};
}
},
},
};
}
/**
* Create a default filesystem instance based on configuration
*/
function createDefaultFilesystem(config) {
try {
// Get base path from config
const basePath = config.workingDirectory || config.rootPath || config.basePath;
if (!basePath) {
throw new Error('rootPath or basePath is required to create a filesystem');
}
// Create semantic backend
const semanticBackend = new DiskSemanticBackend(basePath, {
enableNaturalLanguage: true,
semanticThreshold: 0.5,
chunkingConfig: {
maxChunkSize: 1024,
overlapSize: 128,
},
});
// Initialize backend
semanticBackend.initialize().catch(console.error);
// Create filesystem interface adapter
return {
accessFile: async (params) => {
return semanticBackend.accessFile(params);
},
updateContent: async (params) => {
return semanticBackend.updateContent(params);
},
discoverFiles: async (params) => {
return semanticBackend.discoverFiles(params);
},
organizeFiles: async (params) => {
return semanticBackend.organizeFiles(params);
},
removeFiles: async (params) => {
return semanticBackend.removeFiles(params);
},
executeWorkflow: async (params) => {
return semanticBackend.executeWorkflow(params);
},
interpretNaturalLanguage: async (params) => {
return semanticBackend.interpretNaturalLanguage(params);
},
};
}
catch (error) {
console.error('Failed to create default filesystem:', error);
throw new Error(`Failed to create default filesystem: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
//# sourceMappingURL=mastra.js.map