UNPKG

@posthog/agent

Version:

TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog

267 lines (264 loc) 8.59 kB
import { promises } from 'node:fs'; import { join, extname } from 'node:path'; import z from 'zod'; import { Logger } from './utils/logger.js'; class PostHogFileManager { repositoryPath; logger; constructor(repositoryPath, logger) { this.repositoryPath = repositoryPath; this.logger = logger || new Logger({ debug: false, prefix: "[FileManager]" }); } getTaskDirectory(taskId) { return join(this.repositoryPath, ".posthog", taskId); } getTaskFilePath(taskId, fileName) { return join(this.getTaskDirectory(taskId), fileName); } async ensureTaskDirectory(taskId) { const taskDir = this.getTaskDirectory(taskId); try { await promises.access(taskDir); } catch { await promises.mkdir(taskDir, { recursive: true }); } } async writeTaskFile(taskId, file) { await this.ensureTaskDirectory(taskId); const filePath = this.getTaskFilePath(taskId, file.name); this.logger.debug("Writing task file", { filePath, contentLength: file.content.length, contentType: typeof file.content, }); await promises.writeFile(filePath, file.content, "utf8"); this.logger.debug("File written successfully", { filePath }); } async readTaskFile(taskId, fileName) { try { const filePath = this.getTaskFilePath(taskId, fileName); return await promises.readFile(filePath, "utf8"); } catch (error) { if (error.code === "ENOENT") { return null; } throw error; } } async listTaskFiles(taskId) { try { const taskDir = this.getTaskDirectory(taskId); const files = await promises.readdir(taskDir); return files.filter((file) => !file.startsWith(".")); } catch (error) { if (error.code === "ENOENT") { return []; } throw error; } } async deleteTaskFile(taskId, fileName) { try { const filePath = this.getTaskFilePath(taskId, fileName); await promises.unlink(filePath); } catch (error) { if (error.code !== "ENOENT") { throw error; } } } async taskDirectoryExists(taskId) { try { const taskDir = this.getTaskDirectory(taskId); await promises.access(taskDir); return true; } catch { return false; } } async cleanupTaskDirectory(taskId) { try { const taskDir = this.getTaskDirectory(taskId); await promises.rm(taskDir, { recursive: true, force: true }); } catch (error) { if (error.code !== "ENOENT") { throw error; } } } // Convenience methods for common file types async writePlan(taskId, plan) { this.logger.debug("Writing plan", { taskId, planLength: plan.length, contentPreview: plan.substring(0, 200), }); await this.writeTaskFile(taskId, { name: "plan.md", content: plan, type: "plan", }); this.logger.info("Plan file written", { taskId }); } async readPlan(taskId) { return await this.readTaskFile(taskId, "plan.md"); } async writeContext(taskId, context) { await this.writeTaskFile(taskId, { name: "context.md", content: context, type: "context", }); } async readContext(taskId) { return await this.readTaskFile(taskId, "context.md"); } async writeRequirements(taskId, requirements) { await this.writeTaskFile(taskId, { name: "requirements.md", content: requirements, type: "reference", }); } async readRequirements(taskId) { return await this.readTaskFile(taskId, "requirements.md"); } async writeResearch(taskId, data) { this.logger.debug("Writing research", { taskId, score: data.actionabilityScore, hasQuestions: !!data.questions, questionCount: data.questions?.length ?? 0, answered: data.answered ?? false, }); await this.writeTaskFile(taskId, { name: "research.json", content: JSON.stringify(data, null, 2), type: "artifact", }); this.logger.info("Research file written", { taskId, score: data.actionabilityScore, hasQuestions: !!data.questions, answered: data.answered ?? false, }); } async readResearch(taskId) { try { const content = await this.readTaskFile(taskId, "research.json"); return content ? JSON.parse(content) : null; } catch (error) { this.logger.debug("Failed to parse research.json", { error }); return null; } } async writeTodos(taskId, data) { const todos = z.object({ metadata: z.object({ total: z.number(), completed: z.number(), }), }); const validatedData = todos.parse(data); this.logger.debug("Writing todos", { taskId, total: validatedData.metadata?.total ?? 0, completed: validatedData.metadata?.completed ?? 0, }); await this.writeTaskFile(taskId, { name: "todos.json", content: JSON.stringify(validatedData, null, 2), type: "artifact", }); this.logger.info("Todos file written", { taskId, total: validatedData.metadata?.total ?? 0, completed: validatedData.metadata?.completed ?? 0, }); } async readTodos(taskId) { try { const content = await this.readTaskFile(taskId, "todos.json"); return content ? JSON.parse(content) : null; } catch (error) { this.logger.debug("Failed to parse todos.json", { error }); return null; } } async getTaskFiles(taskId) { const fileNames = await this.listTaskFiles(taskId); const files = []; for (const fileName of fileNames) { const content = await this.readTaskFile(taskId, fileName); if (content !== null) { // Determine type based on file name const type = this.resolveFileType(fileName); files.push({ name: fileName, content, type, created_at: new Date().toISOString(), // Could be enhanced with file stats }); } } return files; } async collectTaskArtifacts(taskId) { const fileNames = await this.listTaskFiles(taskId); const artifacts = []; for (const fileName of fileNames) { const content = await this.readTaskFile(taskId, fileName); if (content === null) { continue; } const type = this.resolveFileType(fileName); const contentType = this.inferContentType(fileName); const size = Buffer.byteLength(content, "utf8"); artifacts.push({ name: fileName, content, type, contentType, size, }); } return artifacts; } resolveFileType(fileName) { if (fileName === "plan.md") return "plan"; if (fileName === "context.md") return "context"; if (fileName === "requirements.md") return "reference"; if (fileName.startsWith("output_")) return "output"; if (fileName.endsWith(".md")) return "reference"; return "artifact"; } inferContentType(fileName) { const extension = extname(fileName).toLowerCase(); switch (extension) { case ".md": return "text/markdown"; case ".json": return "application/json"; case ".txt": return "text/plain"; default: return "text/plain"; } } } export { PostHogFileManager }; //# sourceMappingURL=file-manager.js.map