UNPKG

@plastichub/osr-ai-tools

Version:

CLI and library for LLM tools

368 lines (361 loc) 15.3 kB
import * as path from 'path' import { RunnableToolFunction } from 'openai/lib/RunnableFunction' import { sync as rm } from '@plastichub/fs/remove' //import { filesEx as glob } from '@plastichub/osr-commons/_glob' import { isString } from '@plastichub/core/primitives' import { sync as dir } from '@plastichub/fs/dir' import { sync as write } from '@plastichub/fs/write' import { sync as read } from '@plastichub/fs/read' import { sync as rename } from '@plastichub/fs/rename' import { sync as exists } from '@plastichub/fs/exists' import { filesEx } from '@plastichub/osr-commons/_glob' import { toolLogger } from '../..' import { IKBotTask } from '../../types' import { EXCLUDE_GLOB } from '../../constants' import { glob, globSync, GlobOptions } from 'glob' const isBase64 = (str: string): boolean => { // 1. Quick checks for length & allowed characters: // - Must be multiple of 4 in length // - Must match Base64 charset (A-Z, a-z, 0-9, +, /) plus optional "=" padding if (!str || str.length % 4 !== 0) { return false; } const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/; if (!base64Regex.test(str)) { return false; } // 2. Attempt decode–re-encode to confirm validity: try { const decoded = atob(str); // Decode from Base64 const reencoded = btoa(decoded); // Re-encode to Base64 // Compare the re-encoded string to original return reencoded === str; } catch { return false; } } export const decode_base64 = (base64: string): string => { try { if(!isBase64(base64)) { return base64 } return Buffer.from(base64, 'base64').toString('utf-8'); } catch (error) { throw new Error('Failed to decode base64 string'); } }; export const tools = (target: string, options: IKBotTask): Array<any> => { const logger = toolLogger('fs', options) const category = 'fs' return [ { type: 'function', function: { name: 'list_files', description: 'List all files in a directory', parameters: { type: 'object', properties: { directory: { type: 'string' }, pattern: { type: 'string', optional: true } }, required: ['directory'] }, function: async (params: any) => { try { const directory = path.join(target, params.directory); if (!exists(directory)) { logger.debug(`Tool::ListFiles Directory ${directory} does not exist`); return [] } let pattern = params.pattern || '**/*'; logger.debug(`Tool::ListFiles Listing files in ${directory} with pattern ${pattern}`); pattern = [ ...EXCLUDE_GLOB, pattern ] const ret = await glob(pattern, { cwd: directory, absolute: false, ignore: EXCLUDE_GLOB }); return ret } catch (error) { logger.error('Error listing files', error); throw error; } }, parse: JSON.parse } } as RunnableToolFunction<any>, { type: 'function', function: { name: 'read_files', description: 'Reads files in a directory with a given pattern', parameters: { type: 'object', properties: { directory: { type: 'string' }, pattern: { type: 'string', optional: true } }, required: ['directory'] }, function: async (params: any) => { try { const pattern = params.pattern || '**/*'; let entries = filesEx(target, pattern); let ret = entries.map((entry) => { try { let content = read(entry); return { path: path.relative(target, entry).replace(/\\/g, '/'), content: content.toString() } } catch (error) { logger.error(`Error reading file ${entry}:`, error) return null } }) ret = ret.filter((entry) => (entry !== null && entry.content)) logger.debug(`Tool::ReadFiles Reading files in ${target} with pattern ${pattern} : ${ret.length} files`, ret.map((entry) => entry.path)); return ret } catch (error) { logger.error('Error listing files', error); throw error; } }, parse: JSON.parse } } as RunnableToolFunction<any>, { type: 'function', function: { name: 'remove_file', description: 'Remove a file at given path', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] }, function: async (params: any) => { try { const filePath = path.join(target, params.path); logger.debug(`Tool::RemoveFile Removing file ${filePath}`); rm(filePath); return true; } catch (error) { logger.error('Error removing file', error); throw error; } }, parse: JSON.parse } } as RunnableToolFunction<any>, { type: 'function', function: { name: 'rename_file', description: 'Rename or move a file or directory', parameters: { type: 'object', properties: { src: { type: 'string' }, dst: { type: 'string' } }, required: ['path'] }, function: async (params: any) => { try { const src = path.join(target, params.src) logger.debug(`Tool::Rename file ${src} to ${params.dst}`) rename(src, params.dst) rm(src) return true } catch (error) { logger.error('Error removing file', error) throw error } }, parse: JSON.parse } } as RunnableToolFunction<any>, { type: 'function', function: { name: "modify_project_files", description: "Create or modify existing project files in one shot, preferably used for creating project structure)", parameters: { type: "object", properties: { files: { type: "array", items: { type: "object", properties: { path: { type: "string" }, content: { type: "string", description: "base64 encoded string" } }, required: ["path", "content"] } } }, required: ["files"], }, function: async (ret) => { try { if (!target) { logger.error(`Tool::FS:modify_project_files : Root path required`) return } let { files } = ret as any if (isString(files)) { try { files = JSON.parse(files) } catch (error: any) { logger.error(`Tool::modify_project_files : Structure Error parsing files`, error, ret) write(path.join(target, 'tools-output.json'), files) return error.message } } for (const file of files) { const filePath = path.join(target, file.path); logger.debug(`Tool:modify_project_files writing file ${filePath}`) try { let content = decode_base64(file.content) await write(filePath, content) } catch (error) { logger.error(`Tool:modify_project_files Error writing file`, error) } } } catch (error) { logger.error(`Error creating project structure`, error) } }, parse: JSON.parse, }, } as RunnableToolFunction<{ id: string }>, { type: 'function', function: { name: "write_file", description: "Writes to a file, given a path and content (base64). No directory or file exists check needed!", parameters: { type: "object", properties: { file: { type: "object", properties: { path: { type: "string" }, content: { type: "string", description: "base64 encoded string" } } } }, required: ["file"], }, function: async (params) => { try { if (isString(params)) { try { params = JSON.parse(params) } catch (error: any) { logger.error(`Tool::create_file : Structure Error parsing files`, error, params) return error.message } } let { file } = params as any if (!target || !file.path || !file.content) { logger.error(`Tool::create_file : Path/Target/Content are required to create file`, params) return } let content = decode_base64(file.content) logger.debug(`Tool::create_file Writing file ${file.path} in ${target}`) const filePath = path.join(target, file.path) write(filePath, content) return true } catch (error) { logger.error(`Tool:create_file Error writing file`, error) return false } }, parse: JSON.parse, }, } as RunnableToolFunction<{ id: string }>, { type: 'function', function: { name: "file_exists", description: "check if a file or folder exists", parameters: { type: "object", properties: { file: { type: "object", properties: { path: { type: "string" } } } }, required: ["file"], }, function: async (ret) => { try { if (isString(ret)) { try { ret = JSON.parse(ret) } catch (error: any) { logger.error(`Tool::file_exists : Structure Error parsing files`, error, ret) return error.message } } const { file } = ret as any if (!target || !file.path) { logger.error(`Tool::file_exists : Path is required to `, ret) return } const filePath = path.join(target, file.path) const res = exists(filePath) logger.debug(`Tool::file_exists ${filePath} exists: ${res}`) return res ? true : false } catch (error) { logger.error(`Tool:file_exists error`, error) return false } }, parse: JSON.parse, }, } as RunnableToolFunction<{ id: string }>, { type: 'function', function: { name: "read_file", description: "read a file, at given a path", parameters: { type: "object", properties: { file: { type: "object", properties: { path: { type: "string" } } } }, required: ["file"], }, function: async (ret) => { try { const { file } = ret as any const filePath = path.join(target, file.path) logger.debug(`Tool::ReadFile Reading file ${filePath}`) return read(filePath, 'string') } catch (error) { logger.error(`Error reading file`, error) } }, parse: JSON.parse } } as RunnableToolFunction<{ id: string }> ] };