@plastichub/osr-ai-tools
Version:
CLI and library for LLM tools
368 lines (361 loc) • 15.3 kB
text/typescript
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 }>
]
};