@posthog/agent 1.30.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,499 +0,0 @@
1
- import { promises as fs } from "node:fs";
2
- import { join } from "node:path";
3
- import type { TemplateVariables } from "./template-manager.js";
4
- import type {
5
- PostHogResource,
6
- ResourceType,
7
- SupportingFile,
8
- Task,
9
- UrlMention,
10
- } from "./types.js";
11
- import { Logger } from "./utils/logger.js";
12
-
13
- export interface PromptBuilderDeps {
14
- getTaskFiles: (taskId: string) => Promise<SupportingFile[]>;
15
- generatePlanTemplate: (vars: TemplateVariables) => Promise<string>;
16
- posthogClient?: {
17
- fetchResourceByUrl: (mention: UrlMention) => Promise<PostHogResource>;
18
- };
19
- logger?: Logger;
20
- }
21
-
22
- export class PromptBuilder {
23
- private getTaskFiles: PromptBuilderDeps["getTaskFiles"];
24
- private generatePlanTemplate: PromptBuilderDeps["generatePlanTemplate"];
25
- private posthogClient?: PromptBuilderDeps["posthogClient"];
26
- private logger: Logger;
27
-
28
- constructor(deps: PromptBuilderDeps) {
29
- this.getTaskFiles = deps.getTaskFiles;
30
- this.generatePlanTemplate = deps.generatePlanTemplate;
31
- this.posthogClient = deps.posthogClient;
32
- this.logger =
33
- deps.logger || new Logger({ debug: false, prefix: "[PromptBuilder]" });
34
- }
35
-
36
- /**
37
- * Extract file paths from XML tags in description
38
- * Format: <file path="relative/path.ts" />
39
- */
40
- private extractFilePaths(description: string): string[] {
41
- const fileTagRegex = /<file\s+path="([^"]+)"\s*\/>/g;
42
- const paths: string[] = [];
43
- let match: RegExpExecArray | null;
44
-
45
- match = fileTagRegex.exec(description);
46
- while (match !== null) {
47
- paths.push(match[1]);
48
- match = fileTagRegex.exec(description);
49
- }
50
-
51
- return paths;
52
- }
53
-
54
- /**
55
- * Read file contents from repository
56
- */
57
- private async readFileContent(
58
- repositoryPath: string,
59
- filePath: string,
60
- ): Promise<string | null> {
61
- try {
62
- const fullPath = join(repositoryPath, filePath);
63
- const content = await fs.readFile(fullPath, "utf8");
64
- return content;
65
- } catch (error) {
66
- this.logger.warn(`Failed to read referenced file: ${filePath}`, {
67
- error,
68
- });
69
- return null;
70
- }
71
- }
72
-
73
- /**
74
- * Extract URL mentions from XML tags in description
75
- * Formats: <error id="..." />, <experiment id="..." />, <url href="..." />
76
- */
77
- private extractUrlMentions(description: string): UrlMention[] {
78
- const mentions: UrlMention[] = [];
79
-
80
- // PostHog resource mentions: <error id="..." />, <experiment id="..." />, etc.
81
- const resourceRegex =
82
- /<(error|experiment|insight|feature_flag)\s+id="([^"]+)"\s*\/>/g;
83
- let match: RegExpExecArray | null;
84
-
85
- match = resourceRegex.exec(description);
86
- while (match !== null) {
87
- const [, type, id] = match;
88
- mentions.push({
89
- url: "", // Will be reconstructed if needed
90
- type: type as ResourceType,
91
- id,
92
- label: this.generateUrlLabel("", type as ResourceType),
93
- });
94
- match = resourceRegex.exec(description);
95
- }
96
-
97
- // Generic URL mentions: <url href="..." />
98
- const urlRegex = /<url\s+href="([^"]+)"\s*\/>/g;
99
- match = urlRegex.exec(description);
100
- while (match !== null) {
101
- const [, url] = match;
102
- mentions.push({
103
- url,
104
- type: "generic",
105
- label: this.generateUrlLabel(url, "generic"),
106
- });
107
- match = urlRegex.exec(description);
108
- }
109
-
110
- return mentions;
111
- }
112
-
113
- /**
114
- * Generate a display label for a URL mention
115
- */
116
- private generateUrlLabel(url: string, type: string): string {
117
- try {
118
- const urlObj = new URL(url);
119
- switch (type) {
120
- case "error": {
121
- const errorMatch = url.match(/error_tracking\/([a-f0-9-]+)/);
122
- return errorMatch ? `Error ${errorMatch[1].slice(0, 8)}...` : "Error";
123
- }
124
- case "experiment": {
125
- const expMatch = url.match(/experiments\/(\d+)/);
126
- return expMatch ? `Experiment #${expMatch[1]}` : "Experiment";
127
- }
128
- case "insight":
129
- return "Insight";
130
- case "feature_flag":
131
- return "Feature Flag";
132
- default:
133
- return urlObj.hostname;
134
- }
135
- } catch {
136
- return "URL";
137
- }
138
- }
139
-
140
- /**
141
- * Process URL references and fetch their content
142
- */
143
- private async processUrlReferences(
144
- description: string,
145
- ): Promise<{ description: string; referencedResources: PostHogResource[] }> {
146
- const urlMentions = this.extractUrlMentions(description);
147
- const referencedResources: PostHogResource[] = [];
148
-
149
- if (urlMentions.length === 0 || !this.posthogClient) {
150
- return { description, referencedResources };
151
- }
152
-
153
- // Fetch all referenced resources
154
- for (const mention of urlMentions) {
155
- try {
156
- const resource = await this.posthogClient.fetchResourceByUrl(mention);
157
- referencedResources.push(resource);
158
- } catch (error) {
159
- this.logger.warn(`Failed to fetch resource from URL: ${mention.url}`, {
160
- error,
161
- });
162
- // Add a placeholder resource for failed fetches
163
- referencedResources.push({
164
- type: mention.type,
165
- id: mention.id || "",
166
- url: mention.url,
167
- title: mention.label || "Unknown Resource",
168
- content: `Failed to fetch resource from ${mention.url}: ${error}`,
169
- metadata: {},
170
- });
171
- }
172
- }
173
-
174
- // Replace URL tags with just the label for readability
175
- let processedDescription = description;
176
- for (const mention of urlMentions) {
177
- if (mention.type === "generic") {
178
- // Generic URLs: <url href="..." />
179
- const escapedUrl = mention.url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
180
- processedDescription = processedDescription.replace(
181
- new RegExp(`<url\\s+href="${escapedUrl}"\\s*/>`, "g"),
182
- `@${mention.label}`,
183
- );
184
- } else {
185
- // PostHog resources: <error id="..." />, <experiment id="..." />, etc.
186
- const escapedType = mention.type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
187
- const escapedId = mention.id
188
- ? mention.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
189
- : "";
190
- processedDescription = processedDescription.replace(
191
- new RegExp(`<${escapedType}\\s+id="${escapedId}"\\s*/>`, "g"),
192
- `@${mention.label}`,
193
- );
194
- }
195
- }
196
-
197
- return { description: processedDescription, referencedResources };
198
- }
199
-
200
- /**
201
- * Process description to extract file tags and read contents
202
- * Returns processed description and referenced file contents
203
- */
204
- private async processFileReferences(
205
- description: string,
206
- repositoryPath?: string,
207
- ): Promise<{
208
- description: string;
209
- referencedFiles: Array<{ path: string; content: string }>;
210
- }> {
211
- const filePaths = this.extractFilePaths(description);
212
- const referencedFiles: Array<{ path: string; content: string }> = [];
213
-
214
- if (filePaths.length === 0 || !repositoryPath) {
215
- return { description, referencedFiles };
216
- }
217
-
218
- // Read all referenced files, tracking which ones succeed
219
- const successfulPaths = new Set<string>();
220
- for (const filePath of filePaths) {
221
- const content = await this.readFileContent(repositoryPath, filePath);
222
- if (content !== null) {
223
- referencedFiles.push({ path: filePath, content });
224
- successfulPaths.add(filePath);
225
- }
226
- }
227
-
228
- // Only replace tags for files that were successfully read
229
- let processedDescription = description;
230
- for (const filePath of successfulPaths) {
231
- const fileName = filePath.split("/").pop() || filePath;
232
- processedDescription = processedDescription.replace(
233
- new RegExp(
234
- `<file\\s+path="${filePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"\\s*/>`,
235
- "g",
236
- ),
237
- `@${fileName}`,
238
- );
239
- }
240
-
241
- return { description: processedDescription, referencedFiles };
242
- }
243
-
244
- async buildResearchPrompt(
245
- task: Task,
246
- repositoryPath?: string,
247
- ): Promise<string> {
248
- // Process file references in description
249
- const { description: descriptionAfterFiles, referencedFiles } =
250
- await this.processFileReferences(task.description, repositoryPath);
251
-
252
- // Process URL references in description
253
- const { description: processedDescription, referencedResources } =
254
- await this.processUrlReferences(descriptionAfterFiles);
255
-
256
- let prompt = "<task>\n";
257
- prompt += `<title>${task.title}</title>\n`;
258
- prompt += `<description>${processedDescription}</description>\n`;
259
-
260
- if (task.repository) {
261
- prompt += `<repository>${task.repository}</repository>\n`;
262
- }
263
- prompt += "</task>\n";
264
-
265
- // Add referenced files from @ mentions
266
- if (referencedFiles.length > 0) {
267
- prompt += "\n<referenced_files>\n";
268
- for (const file of referencedFiles) {
269
- prompt += `<file path="${file.path}">\n\`\`\`\n${file.content}\n\`\`\`\n</file>\n`;
270
- }
271
- prompt += "</referenced_files>\n";
272
- }
273
-
274
- // Add referenced resources from URL mentions
275
- if (referencedResources.length > 0) {
276
- prompt += "\n<referenced_resources>\n";
277
- for (const resource of referencedResources) {
278
- prompt += `<resource type="${resource.type}" url="${resource.url}">\n`;
279
- prompt += `<title>${resource.title}</title>\n`;
280
- prompt += `<content>${resource.content}</content>\n`;
281
- prompt += "</resource>\n";
282
- }
283
- prompt += "</referenced_resources>\n";
284
- }
285
-
286
- try {
287
- const taskFiles = await this.getTaskFiles(task.id);
288
- const contextFiles = taskFiles.filter(
289
- (f: SupportingFile) => f.type === "context" || f.type === "reference",
290
- );
291
- if (contextFiles.length > 0) {
292
- prompt += "\n<supporting_files>\n";
293
- for (const file of contextFiles) {
294
- prompt += `<file name="${file.name}" type="${file.type}">\n${file.content}\n</file>\n`;
295
- }
296
- prompt += "</supporting_files>\n";
297
- }
298
- } catch (_error) {
299
- this.logger.debug("No existing task files found for research", {
300
- taskId: task.id,
301
- });
302
- }
303
-
304
- return prompt;
305
- }
306
-
307
- async buildPlanningPrompt(
308
- task: Task,
309
- repositoryPath?: string,
310
- ): Promise<string> {
311
- // Process file references in description
312
- const { description: descriptionAfterFiles, referencedFiles } =
313
- await this.processFileReferences(task.description, repositoryPath);
314
-
315
- // Process URL references in description
316
- const { description: processedDescription, referencedResources } =
317
- await this.processUrlReferences(descriptionAfterFiles);
318
-
319
- let prompt = "<task>\n";
320
- prompt += `<title>${task.title}</title>\n`;
321
- prompt += `<description>${processedDescription}</description>\n`;
322
-
323
- if (task.repository) {
324
- prompt += `<repository>${task.repository}</repository>\n`;
325
- }
326
- prompt += "</task>\n";
327
-
328
- // Add referenced files from @ mentions
329
- if (referencedFiles.length > 0) {
330
- prompt += "\n<referenced_files>\n";
331
- for (const file of referencedFiles) {
332
- prompt += `<file path="${file.path}">\n\`\`\`\n${file.content}\n\`\`\`\n</file>\n`;
333
- }
334
- prompt += "</referenced_files>\n";
335
- }
336
-
337
- // Add referenced resources from URL mentions
338
- if (referencedResources.length > 0) {
339
- prompt += "\n<referenced_resources>\n";
340
- for (const resource of referencedResources) {
341
- prompt += `<resource type="${resource.type}" url="${resource.url}">\n`;
342
- prompt += `<title>${resource.title}</title>\n`;
343
- prompt += `<content>${resource.content}</content>\n`;
344
- prompt += "</resource>\n";
345
- }
346
- prompt += "</referenced_resources>\n";
347
- }
348
-
349
- try {
350
- const taskFiles = await this.getTaskFiles(task.id);
351
- const contextFiles = taskFiles.filter(
352
- (f: SupportingFile) => f.type === "context" || f.type === "reference",
353
- );
354
- if (contextFiles.length > 0) {
355
- prompt += "\n<supporting_files>\n";
356
- for (const file of contextFiles) {
357
- prompt += `<file name="${file.name}" type="${file.type}">\n${file.content}\n</file>\n`;
358
- }
359
- prompt += "</supporting_files>\n";
360
- }
361
- } catch (_error) {
362
- this.logger.debug("No existing task files found for planning", {
363
- taskId: task.id,
364
- });
365
- }
366
-
367
- const templateVariables = {
368
- task_id: task.id,
369
- task_title: task.title,
370
- task_description: processedDescription,
371
- date: new Date().toISOString().split("T")[0],
372
- repository: task.repository || "",
373
- };
374
-
375
- const planTemplate = await this.generatePlanTemplate(templateVariables);
376
-
377
- prompt += "\n<instructions>\n";
378
- prompt +=
379
- "Analyze the codebase and create a detailed implementation plan. Use the template structure below, filling each section with specific, actionable information.\n";
380
- prompt += "</instructions>\n\n";
381
- prompt += "<plan_template>\n";
382
- prompt += planTemplate;
383
- prompt += "\n</plan_template>";
384
-
385
- return prompt;
386
- }
387
-
388
- async buildExecutionPrompt(
389
- task: Task,
390
- repositoryPath?: string,
391
- ): Promise<string> {
392
- // Process file references in description
393
- const { description: descriptionAfterFiles, referencedFiles } =
394
- await this.processFileReferences(task.description, repositoryPath);
395
-
396
- // Process URL references in description
397
- const { description: processedDescription, referencedResources } =
398
- await this.processUrlReferences(descriptionAfterFiles);
399
-
400
- let prompt = "<task>\n";
401
- prompt += `<title>${task.title}</title>\n`;
402
- prompt += `<description>${processedDescription}</description>\n`;
403
-
404
- if (task.repository) {
405
- prompt += `<repository>${task.repository}</repository>\n`;
406
- }
407
- prompt += "</task>\n";
408
-
409
- // Add referenced files from @ mentions
410
- if (referencedFiles.length > 0) {
411
- prompt += "\n<referenced_files>\n";
412
- for (const file of referencedFiles) {
413
- prompt += `<file path="${file.path}">\n\`\`\`\n${file.content}\n\`\`\`\n</file>\n`;
414
- }
415
- prompt += "</referenced_files>\n";
416
- }
417
-
418
- // Add referenced resources from URL mentions
419
- if (referencedResources.length > 0) {
420
- prompt += "\n<referenced_resources>\n";
421
- for (const resource of referencedResources) {
422
- prompt += `<resource type="${resource.type}" url="${resource.url}">\n`;
423
- prompt += `<title>${resource.title}</title>\n`;
424
- prompt += `<content>${resource.content}</content>\n`;
425
- prompt += "</resource>\n";
426
- }
427
- prompt += "</referenced_resources>\n";
428
- }
429
-
430
- try {
431
- const taskFiles = await this.getTaskFiles(task.id);
432
- const hasPlan = taskFiles.some((f: SupportingFile) => f.type === "plan");
433
- const todosFile = taskFiles.find(
434
- (f: SupportingFile) => f.name === "todos.json",
435
- );
436
-
437
- if (taskFiles.length > 0) {
438
- prompt += "\n<context>\n";
439
- for (const file of taskFiles) {
440
- if (file.type === "plan") {
441
- prompt += `<plan>\n${file.content}\n</plan>\n`;
442
- } else if (file.name === "todos.json") {
443
- } else {
444
- prompt += `<file name="${file.name}" type="${file.type}">\n${file.content}\n</file>\n`;
445
- }
446
- }
447
- prompt += "</context>\n";
448
- }
449
-
450
- // Add todos context if resuming work
451
- if (todosFile) {
452
- try {
453
- const todos = JSON.parse(todosFile.content);
454
- if (todos.items && todos.items.length > 0) {
455
- prompt += "\n<previous_todos>\n";
456
- prompt +=
457
- "You previously created the following todo list for this task:\n\n";
458
- for (const item of todos.items) {
459
- const statusIcon =
460
- item.status === "completed"
461
- ? "✓"
462
- : item.status === "in_progress"
463
- ? "▶"
464
- : "○";
465
- prompt += `${statusIcon} [${item.status}] ${item.content}\n`;
466
- }
467
- prompt += `\nProgress: ${todos.metadata.completed}/${todos.metadata.total} completed\n`;
468
- prompt +=
469
- "\nYou can reference this list when resuming work or create an updated list as needed.\n";
470
- prompt += "</previous_todos>\n";
471
- }
472
- } catch (error) {
473
- this.logger.debug("Failed to parse todos.json for context", {
474
- error,
475
- });
476
- }
477
- }
478
-
479
- prompt += "\n<instructions>\n";
480
- if (hasPlan) {
481
- prompt +=
482
- "Implement the changes described in the execution plan. Follow the plan step-by-step and make the necessary file modifications.\n";
483
- } else {
484
- prompt +=
485
- "Implement the changes described in the task. Make the necessary file modifications to complete the task.\n";
486
- }
487
- prompt += "</instructions>";
488
- } catch (_error) {
489
- this.logger.debug("No supporting files found for execution", {
490
- taskId: task.id,
491
- });
492
- prompt += "\n<instructions>\n";
493
- prompt += "Implement the changes described in the task.\n";
494
- prompt += "</instructions>";
495
- }
496
-
497
- return prompt;
498
- }
499
- }