@posthog/agent
Version:
TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog
267 lines (264 loc) • 8.59 kB
JavaScript
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