UNPKG

gaunt-sloth-assistant

Version:

[![Tests and Lint](https://github.com/Galvanized-Pukeko/gaunt-sloth-assistant/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/Galvanized-Pukeko/gaunt-sloth-assistant/actions/workflows/unit-tests.yml) [![Integration Tests](https://github.co

283 lines 10 kB
import { displayError, displayInfo, displaySuccess, displayWarning } from '#src/consoleUtils.js'; import { debugLog } from '#src/debugUtils.js'; import { wrapContent } from '#src/prompt.js'; import { getInstallDir, getProjectDir, stdout } from '#src/systemUtils.js'; import { spawn } from 'node:child_process'; import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import url from 'node:url'; export function toFileSafeString(string) { return string.replace(/[^A-Za-z0-9]/g, '-'); } /** * Returns a formatted date string in the format YYYY-MM-DD_HH-MM-SS using local time * @returns A formatted date string */ export function fileSafeLocalDate() { const date = new Date(); // Format: YYYY-MM-DD_HH-MM-SS using local time directly const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`; } /** * Generates a standardized filename with the format: gth_YYYY-MM-DD_HH-MM-SS_COMMAND.md * @param command - The command that created the file (ASK, REVIEW, PR, etc.) * @returns A standardized filename string */ export function generateStandardFileName(command) { const dateTimeStr = fileSafeLocalDate(); const commandStr = toFileSafeString(command.toUpperCase()); return `gth_${dateTimeStr}_${commandStr}.md`; } export function readFileFromProjectDir(fileName) { const currentDir = getProjectDir(); const filePath = resolve(currentDir, fileName); displayInfo(`Reading file ${filePath}...`); return readFileSyncWithMessages(filePath); } export function readFileFromInstallDir(filePath) { const installDir = getInstallDir(); const installFilePath = resolve(installDir, filePath); try { return readFileSync(installFilePath, { encoding: 'utf8' }); } catch (readFromInstallDirError) { displayError(`The ${installFilePath} not found or can\'t be read.`); throw readFromInstallDirError; } } export function writeFileIfNotExistsWithMessages(filePath, content) { displayInfo(`checking ${filePath} existence`); if (!existsSync(filePath)) { // Create parent directories if they don't exist const parentDir = dirname(filePath); if (!existsSync(parentDir)) { mkdirSync(parentDir, { recursive: true }); } writeFileSync(filePath, content); displaySuccess(`Created ${filePath}`); } else { displayWarning(`${filePath} already exists`); } } export function appendToFile(filePath, content) { try { const parentDir = dirname(filePath); if (!existsSync(parentDir)) { mkdirSync(parentDir, { recursive: true }); } appendFileSync(filePath, content); } catch (e) { displayError(`Failed to append to file ${filePath}: ${e.message}`); } } export function readFileSyncWithMessages(filePath, errorMessageIn, noFileMessage) { const errorMessage = errorMessageIn ?? 'Error reading file at: '; try { return readFileSync(filePath, { encoding: 'utf8' }); } catch (error) { displayError(errorMessage + filePath); if (error.code === 'ENOENT') { displayWarning(noFileMessage ?? 'Please ensure the file exists.'); } else { displayError(error.message); } throw error; } } export async function spawnCommand(command, args, progressMessage, successMessage) { return new Promise((resolve, reject) => { const progressIndicator = new ProgressIndicator(progressMessage, true); const out = { stdout: '', stderr: '' }; const spawned = spawn(command, args); spawned.stdout.on('data', async (stdoutChunk) => { progressIndicator.indicate(); out.stdout += stdoutChunk.toString(); }); spawned.stderr.on('data', (err) => { progressIndicator.indicate(); out.stderr += err.toString(); }); spawned.on('error', (err) => { reject(err.toString()); }); spawned.on('close', (code) => { if (code === 0) { displaySuccess(successMessage); resolve(out.stdout); } else { displayError(`Failed to spawn command with code ${code}`); reject(out.stdout + ' ' + out.stderr); } }); }); } export function getSlothVersion() { // TODO figure out if this can be injected with TS const installDir = getInstallDir(); const jsonPath = resolve(installDir, 'package.json'); const projectJson = readFileSync(jsonPath, { encoding: 'utf8' }); return JSON.parse(projectJson).version; } export class ProgressIndicator { interval = undefined; constructor(initialMessage, manual) { stdout.write(initialMessage); if (!manual) { this.interval = setInterval(this.indicateInner, 1000); } } indicateInner() { stdout.write('.'); } indicate() { if (this.interval) { throw new Error('ProgressIndicator.indicate only to be called in manual mode'); } this.indicateInner(); } stop() { if (this.interval) { clearInterval(this.interval); } stdout.write('\n'); } } // Tool formatting utilities /** * Truncate a string to a maximum length with ellipsis */ export function truncateString(str, maxLength) { if (str.length <= maxLength) { return str; } return str.slice(0, maxLength - 3) + '...'; } /** * Format tool call arguments in a human-readable way */ export function formatToolCallArgs(args) { return Object.entries(args) .map(([key, value]) => { let displayValue; if (typeof value === 'string') { displayValue = value; } else if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { displayValue = JSON.stringify(value); } else { displayValue = String(value); } return `${key}: ${truncateString(displayValue, 50)}`; }) .join(', '); } /** * Format a single tool call with name and arguments */ export function formatToolCall(toolName, args, prefix = '') { const formattedArgs = formatToolCallArgs(args); return `${prefix}${toolName}(${formattedArgs})`; } /** * Format multiple tool calls for display (matches Invocation.ts behavior) */ export function formatToolCalls(toolCalls, maxLength = 255) { const formatted = toolCalls .map((toolCall) => { debugLog(JSON.stringify(toolCall)); const formattedArgs = formatToolCallArgs(toolCall.args || {}); return `${toolCall.name}(${formattedArgs})`; }) .join(', '); // Truncate to maxLength characters if needed return formatted.length > maxLength ? formatted.slice(0, maxLength - 3) + '...' : formatted; } /** * Extracts the content of the last message from an LLM response * @param output - The output from the LLM containing messages * @returns The content of the last message */ export function extractLastMessageContent(output) { if (!output || !output.messages || !output.messages.length) { return ''; } return output.messages[output.messages.length - 1].content; } /** * Dynamically imports a module from a file path from the outside of the installation dir * @returns A promise that resolves to the imported module */ export function importExternalFile(filePath) { const configFileUrl = url.pathToFileURL(filePath).toString(); return import(configFileUrl); } /** * Alias for importExternalFile for backward compatibility with tests * @param filePath - The path to the file to import * @returns A promise that resolves to the imported module */ export const importFromFilePath = importExternalFile; /** * Reads multiple files from the current directory and returns their contents * @param fileNames - Array of file names to read * @returns Combined content of all files with proper formatting, each file is wrapped in random block like <file-abvesde> */ export function readMultipleFilesFromProjectDir(fileNames) { if (!Array.isArray(fileNames)) { return wrapContent(readFileFromProjectDir(fileNames), 'file', `file ${fileNames}`, true); } return fileNames .map((fileName) => { const content = readFileFromProjectDir(fileName); return `${wrapContent(content, 'file', `file ${fileName}`, true)}`; }) .join('\n\n'); } export async function execAsync(command) { const { exec } = await import('node:child_process'); return new Promise((resolve, reject) => { exec(command, (error, stdout, stderr) => { if (error) { reject(error); return; } if (stderr) { reject(new Error(stderr)); return; } resolve(stdout.trim()); }); }); } /** * Utility function to execute hook(s) - either a single hook or an array of hooks * Fully type-safe and works with any number of arguments * @param hooks - Single hook function or array of hook functions (or undefined) * @param args - Arguments to pass to each hook function */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function executeHooks(hooks, ...args) { if (!hooks) return; if (Array.isArray(hooks)) { for (const hook of hooks) { await hook(...args); } } else { await hooks(...args); } } //# sourceMappingURL=utils.js.map