import * as yaml from 'yaml'; import { readdir } from 'fs/promises'; import { libx } from 'libx.js/build/bundles/node.essentials'; import { di } from 'libx.js/build/modules/dependencyInjector'; import fs from 'fs'; import path from 'path'; import url from 'url'; import { FeedoxAIModule } from "../providers/FeedoxAI"; // Helper class for common functions class Helpers { private static runIdAlreadyGenerated = false; public di = di; public constructor() { } // Parse YAML block from LLM output public extractYaml(text: string): any { const unescaped = text .replace(/\\`/g, '`') // turn \\` into ` .replace(/\\n/g, '\n') // turn \n into real newlines .replace(/\\"/g, '"'); // turn escaped quotes back to quotes const match = unescaped.match(/```yaml([\s\S]*?)```/); if (!match) throw new Error(`No YAML block found in LLM output. \nText: ${text}`); return yaml.parse(match[1]); } // Utility to list available actions in 'body/' folder public async listActions(): Promise { const files = await readdir('./body'); return files.filter(f => f.endsWith('.ts') || f.endsWith('.sh')); } // Generate prompt text including available actions and previous output public buildActionSelectionPrompt(actions: string[], prevOutput: string): string { return ` You are an AI agent that selects the next action to perform. Available actions: ${actions.map(a => `- ${a}`).join('\n')} Previous output/context: ${prevOutput} Respond ONLY with a YAML block specifying: \`\`\`yaml next_action: command: "" params: - "" - "" message: "" \`\`\` `; } public async quickAsk(input: string, modelOverride?: string) { const { AskModule } = await import('../ask'); const askModule = new AskModule(this.di.modules.ai); const res = await AskModule.collectAllChunks( askModule.ask(input, null, { model: modelOverride }) ); return res; } public addLineNumbers(content: string): string { return content .split('\n') .map((line, idx) => `${idx + 1}:\t${line}`) .join('\n'); } public format(textTemplate: string, data: any) { return libx.extensions.string.format.call(textTemplate, data); } public getCurrentRunId() { const curRunFile = path.join(process.cwd(), '.tmp/current-run'); let runId: string = null if (libx.isString(libx.node.args.n)) { runId = libx.node.args.n; } else { if (fs.existsSync(curRunFile)) { runId = fs.readFileSync(curRunFile, 'utf8'); libx.log.v('reusing runId: ', runId); } else { libx.log.w('no current-run file found', curRunFile); } } if ((libx.isEmpty(runId) || runId.length == 0 || libx.node.args.n) && !Helpers.runIdAlreadyGenerated) { runId = helpers.generateRunId(); // Ensure .tmp directory exists before writing libx.node.mkdirRecursiveSync(path.dirname(curRunFile)); fs.writeFileSync(curRunFile, runId); libx.log.v('new runId: ', runId); Helpers.runIdAlreadyGenerated = true; } return runId; } public generateRunId(date: Date = new Date()) { return this.getTimestamp(date) + '-' + libx.newGuid(); } public getTimestamp(date: Date = new Date()) { return libx.extensions.date.format.call(new Date(), 'yymmdd_HHMMss'); } } export const helpers = new Helpers();