@posthog/agent
Version:
TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog
261 lines (259 loc) • 9.61 kB
JavaScript
class PostHogAPIClient {
config;
constructor(config) {
this.config = config;
}
get baseUrl() {
const host = this.config.apiUrl.endsWith("/")
? this.config.apiUrl.slice(0, -1)
: this.config.apiUrl;
return host;
}
get headers() {
return {
Authorization: `Bearer ${this.config.apiKey}`,
"Content-Type": "application/json",
};
}
async apiRequest(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
...this.headers,
...options.headers,
},
});
if (!response.ok) {
let errorMessage;
try {
const errorResponse = await response.json();
errorMessage = `Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`;
}
catch {
errorMessage = `Failed request: [${response.status}] ${response.statusText}`;
}
throw new Error(errorMessage);
}
return response.json();
}
getTeamId() {
return this.config.projectId;
}
getBaseUrl() {
return this.baseUrl;
}
getApiKey() {
return this.config.apiKey;
}
getLlmGatewayUrl() {
const teamId = this.getTeamId();
return `${this.baseUrl}/api/projects/${teamId}/llm_gateway`;
}
async fetchTask(taskId) {
const teamId = this.getTeamId();
return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/`);
}
async listTasks(filters) {
const teamId = this.getTeamId();
const url = new URL(`${this.baseUrl}/api/projects/${teamId}/tasks/`);
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value)
url.searchParams.append(key, value);
});
}
const response = await this.apiRequest(url.pathname + url.search);
return response.results || [];
}
async updateTask(taskId, updates) {
const teamId = this.getTeamId();
return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/`, {
method: "PATCH",
body: JSON.stringify(updates),
});
}
async createTask(payload) {
const teamId = this.getTeamId();
return this.apiRequest(`/api/projects/${teamId}/tasks/`, {
method: "POST",
body: JSON.stringify({
origin_product: "user_created",
...payload,
}),
});
}
// TaskRun methods
async listTaskRuns(taskId) {
const teamId = this.getTeamId();
const response = await this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/runs/`);
return response.results || [];
}
async getTaskRun(taskId, runId) {
const teamId = this.getTeamId();
return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`);
}
async createTaskRun(taskId, payload) {
const teamId = this.getTeamId();
return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/runs/`, {
method: "POST",
body: JSON.stringify(payload || {}),
});
}
async updateTaskRun(taskId, runId, payload) {
const teamId = this.getTeamId();
return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`, {
method: "PATCH",
body: JSON.stringify(payload),
});
}
async setTaskRunOutput(taskId, runId, output) {
const teamId = this.getTeamId();
return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/set_output/`, {
method: "PATCH",
body: JSON.stringify({ output }),
});
}
async appendTaskRunLog(taskId, runId, entries) {
const teamId = this.getTeamId();
return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/append_log/`, {
method: "POST",
body: JSON.stringify({ entries }),
});
}
async uploadTaskArtifacts(taskId, runId, artifacts) {
if (!artifacts.length) {
return [];
}
const teamId = this.getTeamId();
const response = await this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/`, {
method: "POST",
body: JSON.stringify({ artifacts }),
});
return response.artifacts ?? [];
}
/**
* Fetch logs from S3 using presigned URL from TaskRun
* @param taskRun - The task run containing the log_url
* @returns Array of stored entries, or empty array if no logs available
*/
async fetchTaskRunLogs(taskRun) {
if (!taskRun.log_url) {
return [];
}
try {
const response = await fetch(taskRun.log_url);
if (!response.ok) {
throw new Error(`Failed to fetch logs: ${response.status} ${response.statusText}`);
}
const content = await response.text();
if (!content.trim()) {
return [];
}
// Parse newline-delimited JSON
return content
.trim()
.split("\n")
.map((line) => JSON.parse(line));
}
catch (error) {
throw new Error(`Failed to fetch task run logs: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Fetch error details from PostHog error tracking
*/
async fetchErrorDetails(errorId, projectId) {
const teamId = projectId ? parseInt(projectId, 10) : this.getTeamId();
try {
const errorData = await this.apiRequest(`/api/projects/${teamId}/error_tracking/${errorId}/`);
// Format error details for agent consumption
const content = this.formatErrorContent(errorData);
return {
type: "error",
id: errorId,
url: `${this.baseUrl}/project/${teamId}/error_tracking/${errorId}`,
title: (typeof errorData.exception_type === "string"
? errorData.exception_type
: undefined) || "Unknown Error",
content,
metadata: {
exception_type: errorData.exception_type,
first_seen: errorData.first_seen,
last_seen: errorData.last_seen,
volume: errorData.volume,
users_affected: errorData.users_affected,
},
};
}
catch (error) {
throw new Error(`Failed to fetch error details for ${errorId}: ${error}`);
}
}
/**
* Generic resource fetcher by URL or ID
*/
async fetchResourceByUrl(urlMention) {
switch (urlMention.type) {
case "error": {
if (!urlMention.id) {
throw new Error("Error ID is required for error resources");
}
// Extract project ID from URL if available, otherwise use default team
let projectId;
if (urlMention.url) {
const projectIdMatch = urlMention.url.match(/\/project\/(\d+)\//);
projectId = projectIdMatch ? projectIdMatch[1] : undefined;
}
return this.fetchErrorDetails(urlMention.id, projectId);
}
case "experiment":
case "insight":
case "feature_flag":
throw new Error(`Resource type '${urlMention.type}' not yet implemented`);
case "generic":
// Return a minimal resource for generic URLs
return {
type: "generic",
id: "",
url: urlMention.url,
title: "Generic Resource",
content: `Generic resource: ${urlMention.url}`,
metadata: {},
};
default:
throw new Error(`Unknown resource type: ${urlMention.type}`);
}
}
/**
* Format error data for agent consumption
*/
formatErrorContent(errorData) {
const sections = [];
if (errorData.exception_type) {
sections.push(`**Error Type**: ${errorData.exception_type}`);
}
if (errorData.exception_message) {
sections.push(`**Message**: ${errorData.exception_message}`);
}
if (errorData.stack_trace) {
sections.push(`**Stack Trace**:\n\`\`\`\n${errorData.stack_trace}\n\`\`\``);
}
if (errorData.volume) {
sections.push(`**Volume**: ${errorData.volume} occurrences`);
}
if (errorData.users_affected) {
sections.push(`**Users Affected**: ${errorData.users_affected}`);
}
if (errorData.first_seen && errorData.last_seen) {
sections.push(`**First Seen**: ${errorData.first_seen}`);
sections.push(`**Last Seen**: ${errorData.last_seen}`);
}
if (errorData.properties && Object.keys(errorData.properties).length > 0) {
sections.push(`**Properties**: ${JSON.stringify(errorData.properties, null, 2)}`);
}
return sections.join("\n\n");
}
}
export { PostHogAPIClient };
//# sourceMappingURL=posthog-api.js.map