packfs-core
Version:
Semantic filesystem operations for LLM agent frameworks with natural language understanding. See LLM_AGENT_GUIDE.md for copy-paste examples.
373 lines • 14.1 kB
JavaScript
/**
* Native Mastra integration for PackFS
* Provides tool factory pattern for seamless Mastra agent integration
*/
import { isAbsolute } from 'path';
import { createTool } from '@mastra/core/tools';
import { DiskBackend } from '../../backends/disk.js';
import { MastraSecurityValidator } from './security/validator.js';
import { schemas, outputSchemas } from './schemas/index.js';
/**
* Create a PackFS tool set for Mastra agents
*
* @param config Configuration for tool generation and security
* @returns Object containing requested tools based on permissions
*/
export function createPackfsTools(config) {
// Validate configuration
validateConfig(config);
// Create filesystem backend
const backend = new DiskBackend(config.rootPath);
// Create security validator
const securityConfig = {
rootPath: config.rootPath,
...config.security
};
const validator = new MastraSecurityValidator(securityConfig);
// Create tool context
const toolContext = {
validator,
filesystem: backend,
config
};
const tools = {};
// Generate tools based on permissions
if (config.permissions.includes('read')) {
tools.fileReader = createFileReaderTool(toolContext);
}
if (config.permissions.includes('write')) {
tools.fileWriter = createFileWriterTool(toolContext);
}
if (config.permissions.includes('search')) {
tools.fileSearcher = createFileSearcherTool(toolContext);
}
if (config.permissions.includes('list')) {
tools.fileLister = createFileListerTool(toolContext);
}
return tools;
}
/**
* Validate tool configuration
*/
function validateConfig(config) {
if (!config.rootPath) {
throw new Error('rootPath is required');
}
if (!isAbsolute(config.rootPath)) {
throw new Error('rootPath must be absolute');
}
if (!config.permissions || config.permissions.length === 0) {
throw new Error('At least one permission must be specified');
}
// Validate permissions
const validPermissions = ['read', 'write', 'search', 'list'];
for (const permission of config.permissions) {
if (!validPermissions.includes(permission)) {
throw new Error(`Invalid permission: ${permission}`);
}
}
}
/**
* Create file reader tool for accessing file content and metadata
*/
function createFileReaderTool(context) {
return createTool({
id: 'packfs-file-reader',
description: 'Read files and directories through PackFS with security validation',
inputSchema: context.config.schemas?.access ?? schemas.access,
outputSchema: outputSchemas.file,
execute: async ({ context: input }) => {
const startTime = Date.now();
try {
// Validate input
const validatedInput = (context.config.schemas?.access ?? schemas.access).parse(input);
// Security validation
const validation = context.validator.validateOperation(validatedInput);
if (!validation.valid) {
return {
success: false,
error: validation.reason,
executionMetadata: {
executionTime: Date.now() - startTime,
operationType: validatedInput.purpose
}
};
}
// Execute operation
let result;
switch (validatedInput.purpose) {
case 'read':
result = await executeReadOperation(context, validatedInput);
break;
case 'metadata':
result = await executeMetadataOperation(context, validatedInput);
break;
case 'exists':
result = await executeExistsOperation(context, validatedInput);
break;
}
return {
success: true,
...result,
executionMetadata: {
executionTime: Date.now() - startTime,
filesAccessed: [validatedInput.target.path],
operationType: validatedInput.purpose
}
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
executionMetadata: {
executionTime: Date.now() - startTime
}
};
}
}
});
}
/**
* Create file writer tool for creating and modifying files
*/
function createFileWriterTool(context) {
return createTool({
id: 'packfs-file-writer',
description: 'Create and update files through PackFS with security validation',
inputSchema: context.config.schemas?.update ?? schemas.update,
outputSchema: outputSchemas.update,
execute: async ({ context: input }) => {
const startTime = Date.now();
try {
// Validate input
const validatedInput = (context.config.schemas?.update ?? schemas.update).parse(input);
// Security validation
const validation = context.validator.validateOperation(validatedInput);
if (!validation.valid) {
return {
success: false,
error: validation.reason,
executionMetadata: {
executionTime: Date.now() - startTime,
operationType: validatedInput.purpose
}
};
}
// Execute operation
let result;
switch (validatedInput.purpose) {
case 'create':
case 'update':
result = await executeWriteOperation(context, validatedInput);
break;
case 'append':
result = await executeAppendOperation(context, validatedInput);
break;
case 'delete':
result = await executeDeleteOperation(context, validatedInput);
break;
}
return {
success: true,
...result,
executionMetadata: {
executionTime: Date.now() - startTime,
filesAccessed: [validatedInput.target.path],
operationType: validatedInput.purpose
}
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
executionMetadata: {
executionTime: Date.now() - startTime
}
};
}
}
});
}
/**
* Create file searcher tool for finding files by content
*/
function createFileSearcherTool(context) {
return createTool({
id: 'packfs-file-searcher',
description: 'Search files and content through PackFS with semantic capabilities',
inputSchema: context.config.schemas?.discover ?? schemas.discover,
outputSchema: outputSchemas.search,
execute: async ({ context: input }) => {
const startTime = Date.now();
try {
// Validate input
const validatedInput = (context.config.schemas?.discover ?? schemas.discover).parse(input);
// Security validation
const validation = context.validator.validateOperation(validatedInput);
if (!validation.valid) {
return {
success: false,
error: validation.reason,
results: [],
totalResults: 0,
metadata: {
executionTime: Date.now() - startTime,
operationType: validatedInput.purpose
}
};
}
// Execute search operation
const result = await executeSearchOperation(context, validatedInput);
return {
success: true,
...result,
executionMetadata: {
executionTime: Date.now() - startTime,
filesAccessed: [validatedInput.target.path],
operationType: validatedInput.purpose
}
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
results: [],
totalResults: 0,
metadata: {
executionTime: Date.now() - startTime
}
};
}
}
});
}
/**
* Create file lister tool for directory listing
*/
function createFileListerTool(context) {
return createTool({
id: 'packfs-file-lister',
description: 'List directory contents through PackFS',
inputSchema: context.config.schemas?.discover ?? schemas.discover,
outputSchema: outputSchemas.search,
execute: async ({ context: input, runtimeContext }) => {
// Force purpose to 'list' for lister tool
const listInput = { ...input, purpose: 'list' };
const searcherTool = createFileSearcherTool(context);
if (!searcherTool.execute) {
throw new Error('File searcher tool does not have execute method');
}
return searcherTool.execute({ context: listInput, runtimeContext });
}
});
}
// Helper functions for operation execution
async function executeReadOperation(context, intent) {
const content = await context.filesystem.read(intent.target.path);
const encoding = intent.target.encoding || 'utf8';
return {
content: content.toString(encoding),
exists: true
};
}
async function executeMetadataOperation(context, intent) {
const exists = await context.filesystem.exists(intent.target.path);
if (!exists) {
return { exists: false };
}
const stats = await context.filesystem.stat(intent.target.path);
return {
exists: true,
metadata: {
size: stats.size,
modified: stats.mtime.toISOString(),
type: stats.isDirectory ? 'directory' : 'file'
}
};
}
async function executeExistsOperation(context, intent) {
const exists = await context.filesystem.exists(intent.target.path);
return { exists };
}
async function executeWriteOperation(context, intent) {
const exists = await context.filesystem.exists(intent.target.path);
const isCreate = intent.purpose === 'create' || !exists;
// Note: Creating parent directories would require additional backend support
// For now, assume the path is valid
await context.filesystem.write(intent.target.path, Buffer.from(intent.content, 'utf8'));
return {
created: isCreate,
path: intent.target.path
};
}
async function executeAppendOperation(context, intent) {
// Read existing content
let existingContent = '';
const exists = await context.filesystem.exists(intent.target.path);
if (exists) {
const buffer = await context.filesystem.read(intent.target.path);
existingContent = buffer.toString('utf8');
}
// Append new content
const newContent = existingContent + intent.content;
await context.filesystem.write(intent.target.path, Buffer.from(newContent, 'utf8'));
return {
created: !exists,
path: intent.target.path
};
}
async function executeDeleteOperation(context, intent) {
const exists = await context.filesystem.exists(intent.target.path);
if (exists) {
await context.filesystem.delete(intent.target.path);
}
return {
deleted: exists,
path: intent.target.path
};
}
async function executeSearchOperation(context, intent) {
const { target, options = {} } = intent;
if (intent.purpose === 'list') {
// Simple directory listing
const files = await context.filesystem.list(target.path);
const results = [];
for (const file of files) {
const filePath = `${target.path}/${file}`;
try {
const stats = await context.filesystem.stat(filePath);
results.push({
path: filePath,
type: stats.isDirectory ? 'directory' : 'file',
metadata: {
size: stats.size,
modified: stats.mtime.toISOString()
}
});
}
catch (error) {
// Skip files that can't be stat'd
continue;
}
if (options.maxResults && results.length >= options.maxResults) {
break;
}
}
return {
results,
totalResults: results.length
};
}
// For content and semantic search, we'd need more complex implementation
// For now, return empty results
return {
results: [],
totalResults: 0
};
}
export { MastraSecurityValidator } from './security/validator.js';
export { schemas, outputSchemas } from './schemas/index.js';
//# sourceMappingURL=index.js.map