@posthog/agent 1.16.0 → 1.16.2

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.
@@ -97,6 +97,8 @@ export class GitManager {
97
97
  }
98
98
 
99
99
  async createOrSwitchToBranch(branchName: string, baseBranch?: string): Promise<void> {
100
+ await this.ensureCleanWorkingDirectory('switching branches');
101
+
100
102
  const exists = await this.branchExists(branchName);
101
103
  if (exists) {
102
104
  await this.switchToBranch(branchName);
@@ -118,15 +120,7 @@ export class GitManager {
118
120
  authorName?: string;
119
121
  authorEmail?: string;
120
122
  }): Promise<string> {
121
- let command = 'commit -m "' + message.replace(/"/g, '\\"') + '"';
122
-
123
- const authorName = options?.authorName || this.authorName;
124
- const authorEmail = options?.authorEmail || this.authorEmail;
125
-
126
- if (authorName && authorEmail) {
127
- command += ` --author="${authorName} <${authorEmail}>"`;
128
- }
129
-
123
+ const command = this.buildCommitCommand(message, options);
130
124
  return await this.runGitCommand(command);
131
125
  }
132
126
 
@@ -148,6 +142,56 @@ export class GitManager {
148
142
  }
149
143
  }
150
144
 
145
+ // Helper: Centralized safety check for uncommitted changes
146
+ private async ensureCleanWorkingDirectory(operation: string): Promise<void> {
147
+ if (await this.hasChanges()) {
148
+ throw new Error(`Uncommitted changes detected. Please commit or stash changes before ${operation}.`);
149
+ }
150
+ }
151
+
152
+ private async generateUniqueBranchName(baseName: string): Promise<string> {
153
+ if (!await this.branchExists(baseName)) {
154
+ return baseName;
155
+ }
156
+
157
+ let counter = 1;
158
+ let uniqueName = `${baseName}-${counter}`;
159
+ while (await this.branchExists(uniqueName)) {
160
+ counter++;
161
+ uniqueName = `${baseName}-${counter}`;
162
+ }
163
+ return uniqueName;
164
+ }
165
+
166
+ private async ensureOnDefaultBranch(): Promise<string> {
167
+ const defaultBranch = await this.getDefaultBranch();
168
+ const currentBranch = await this.getCurrentBranch();
169
+
170
+ if (currentBranch !== defaultBranch) {
171
+ await this.ensureCleanWorkingDirectory('switching to default branch');
172
+ await this.switchToBranch(defaultBranch);
173
+ }
174
+
175
+ return defaultBranch;
176
+ }
177
+
178
+ private buildCommitCommand(message: string, options?: { allowEmpty?: boolean; authorName?: string; authorEmail?: string }): string {
179
+ let command = `commit -m "${message.replace(/"/g, '\\"')}"`;
180
+
181
+ if (options?.allowEmpty) {
182
+ command += ' --allow-empty';
183
+ }
184
+
185
+ const authorName = options?.authorName || this.authorName;
186
+ const authorEmail = options?.authorEmail || this.authorEmail;
187
+
188
+ if (authorName && authorEmail) {
189
+ command += ` --author="${authorName} <${authorEmail}>"`;
190
+ }
191
+
192
+ return command;
193
+ }
194
+
151
195
  async getRemoteUrl(): Promise<string | null> {
152
196
  try {
153
197
  return await this.runGitCommand('remote get-url origin');
@@ -161,75 +205,120 @@ export class GitManager {
161
205
  await this.runGitCommand(`push ${forceFlag} -u origin ${branchName}`);
162
206
  }
163
207
 
164
- // Utility methods for PostHog task execution
165
- async createTaskPlanningBranch(taskId: string, baseBranch?: string): Promise<string> {
166
- let branchName = `posthog/task-${taskId}-planning`;
167
- let counter = 1;
208
+ /**
209
+ * Tracks whether commits were made during an operation by comparing HEAD SHA
210
+ * before and after. Returns an object with methods to finalize the operation.
211
+ *
212
+ * Usage:
213
+ * const tracker = await gitManager.trackCommitsDuring();
214
+ * // ... do work that might create commits ...
215
+ * const result = await tracker.finalize({ commitMessage: 'fallback message', push: true });
216
+ */
217
+ async trackCommitsDuring(): Promise<{
218
+ finalize: (options: {
219
+ commitMessage: string;
220
+ push?: boolean;
221
+ }) => Promise<{ commitCreated: boolean; pushedBranch: boolean }>;
222
+ }> {
223
+ const initialSha = await this.getCommitSha('HEAD');
168
224
 
169
- // Find a unique branch name if the base name already exists
170
- while (await this.branchExists(branchName)) {
171
- branchName = `posthog/task-${taskId}-planning-${counter}`;
172
- counter++;
173
- }
225
+ return {
226
+ finalize: async (options) => {
227
+ const currentSha = await this.getCommitSha('HEAD');
228
+ const externalCommitsCreated = initialSha !== currentSha;
229
+ const hasUncommittedChanges = await this.hasChanges();
230
+
231
+ // If no commits and no changes, nothing to do
232
+ if (!externalCommitsCreated && !hasUncommittedChanges) {
233
+ return { commitCreated: false, pushedBranch: false };
234
+ }
174
235
 
175
- this.logger.debug('Creating unique planning branch', { branchName, taskId });
236
+ let commitCreated = externalCommitsCreated;
176
237
 
177
- // If no base branch specified, ensure we're on main/master
178
- if (!baseBranch) {
179
- baseBranch = await this.getDefaultBranch();
180
- await this.switchToBranch(baseBranch);
238
+ // Commit any remaining uncommitted changes
239
+ if (hasUncommittedChanges) {
240
+ await this.runGitCommand('add .');
241
+ const hasStagedChanges = await this.hasStagedChanges();
181
242
 
182
- // Check for uncommitted changes
183
- if (await this.hasChanges()) {
184
- throw new Error(`Uncommitted changes detected. Please commit or stash changes before running tasks.`);
243
+ if (hasStagedChanges) {
244
+ await this.commitChanges(options.commitMessage);
245
+ commitCreated = true;
246
+ }
247
+ }
248
+
249
+ // Push if requested and commits were made
250
+ let pushedBranch = false;
251
+ if (options.push && commitCreated) {
252
+ const currentBranch = await this.getCurrentBranch();
253
+ await this.pushBranch(currentBranch);
254
+ pushedBranch = true;
255
+ this.logger.info('Pushed branch after operation', { branch: currentBranch });
256
+ }
257
+
258
+ return { commitCreated, pushedBranch };
185
259
  }
186
- }
260
+ };
261
+ }
262
+
263
+ async createTaskBranch(taskSlug: string): Promise<string> {
264
+ const branchName = `posthog/task-${taskSlug}`;
265
+
266
+ // Ensure we're on default branch before creating task branch
267
+ const defaultBranch = await this.ensureOnDefaultBranch();
268
+
269
+ this.logger.info('Creating task branch from default branch', {
270
+ branchName,
271
+ taskSlug,
272
+ baseBranch: defaultBranch
273
+ });
274
+
275
+ await this.createOrSwitchToBranch(branchName, defaultBranch);
187
276
 
188
- await this.createBranch(branchName, baseBranch); // Use createBranch instead of createOrSwitchToBranch for new branches
189
277
  return branchName;
190
278
  }
191
279
 
192
- async createTaskImplementationBranch(taskId: string, planningBranchName?: string): Promise<string> {
193
- let branchName = `posthog/task-${taskId}-implementation`;
194
- let counter = 1;
280
+ async createTaskPlanningBranch(taskId: string, baseBranch?: string): Promise<string> {
281
+ const baseName = `posthog/task-${taskId}-planning`;
282
+ const branchName = await this.generateUniqueBranchName(baseName);
195
283
 
196
- // Find a unique branch name if the base name already exists
197
- while (await this.branchExists(branchName)) {
198
- branchName = `posthog/task-${taskId}-implementation-${counter}`;
199
- counter++;
200
- }
284
+ this.logger.debug('Creating unique planning branch', { branchName, taskId });
285
+
286
+ const base = baseBranch || await this.ensureOnDefaultBranch();
287
+ await this.createBranch(branchName, base);
288
+
289
+ return branchName;
290
+ }
291
+
292
+ async createTaskImplementationBranch(taskId: string, planningBranchName?: string): Promise<string> {
293
+ const baseName = `posthog/task-${taskId}-implementation`;
294
+ const branchName = await this.generateUniqueBranchName(baseName);
201
295
 
202
- const currentBranchBefore = await this.getCurrentBranch();
203
296
  this.logger.debug('Creating unique implementation branch', {
204
297
  branchName,
205
298
  taskId,
206
- currentBranch: currentBranchBefore
299
+ currentBranch: await this.getCurrentBranch()
207
300
  });
208
301
 
209
- // Implementation branch should branch from the specific planning branch
302
+ // Determine base branch: explicit param > current planning branch > default
210
303
  let baseBranch = planningBranchName;
211
304
 
212
305
  if (!baseBranch) {
213
- // Try to find the corresponding planning branch
214
306
  const currentBranch = await this.getCurrentBranch();
215
307
  if (currentBranch.includes('-planning')) {
216
- baseBranch = currentBranch; // Use current planning branch
308
+ baseBranch = currentBranch;
217
309
  this.logger.debug('Using current planning branch', { baseBranch });
218
310
  } else {
219
- // Fallback to default branch
220
- baseBranch = await this.getDefaultBranch();
221
- this.logger.debug('No planning branch found, using default', { baseBranch });
222
- await this.switchToBranch(baseBranch);
311
+ baseBranch = await this.ensureOnDefaultBranch();
312
+ this.logger.debug('Using default branch', { baseBranch });
223
313
  }
224
314
  }
225
315
 
226
316
  this.logger.debug('Creating implementation branch from base', { baseBranch, branchName });
227
- await this.createBranch(branchName, baseBranch); // Create fresh branch from base
317
+ await this.createBranch(branchName, baseBranch);
228
318
 
229
- const currentBranchAfter = await this.getCurrentBranch();
230
319
  this.logger.info('Implementation branch created', {
231
320
  branchName,
232
- currentBranch: currentBranchAfter
321
+ currentBranch: await this.getCurrentBranch()
233
322
  });
234
323
 
235
324
  return branchName;
@@ -323,6 +412,7 @@ Generated by PostHog Agent`;
323
412
  ): Promise<string> {
324
413
  const currentBranch = await this.getCurrentBranch();
325
414
  if (currentBranch !== branchName) {
415
+ await this.ensureCleanWorkingDirectory('creating PR');
326
416
  await this.switchToBranch(branchName);
327
417
  }
328
418
 
@@ -366,31 +456,19 @@ Generated by PostHog Agent`;
366
456
 
367
457
  async commitAndPush(message: string, options?: { allowEmpty?: boolean }): Promise<void> {
368
458
  const hasChanges = await this.hasStagedChanges();
369
-
459
+
370
460
  if (!hasChanges && !options?.allowEmpty) {
371
461
  this.logger.debug('No changes to commit, skipping');
372
462
  return;
373
463
  }
374
464
 
375
- let command = `commit -m "${message.replace(/"/g, '\\"')}"`;
376
-
377
- if (options?.allowEmpty) {
378
- command += ' --allow-empty';
379
- }
380
-
381
- const authorName = this.authorName;
382
- const authorEmail = this.authorEmail;
383
-
384
- if (authorName && authorEmail) {
385
- command += ` --author="${authorName} <${authorEmail}>"`;
386
- }
387
-
465
+ const command = this.buildCommitCommand(message, options);
388
466
  await this.runGitCommand(command);
389
-
467
+
390
468
  // Push to origin
391
469
  const currentBranch = await this.getCurrentBranch();
392
470
  await this.pushBranch(currentBranch);
393
-
471
+
394
472
  this.logger.info('Committed and pushed changes', { branch: currentBranch, message });
395
473
  }
396
474
  }
@@ -1,7 +1,7 @@
1
1
  import { generateObject } from 'ai';
2
- import { anthropic } from '@ai-sdk/anthropic';
3
2
  import { z } from 'zod';
4
3
  import { Logger } from './utils/logger.js';
4
+ import { getAnthropicModel } from './utils/ai-sdk.js';
5
5
 
6
6
  export interface ExtractedQuestion {
7
7
  id: string;
@@ -41,27 +41,34 @@ export interface StructuredExtractor {
41
41
  extractQuestionsWithAnswers(researchContent: string): Promise<ExtractedQuestionWithAnswer[]>;
42
42
  }
43
43
 
44
+ export type StructuredExtractorConfig = {
45
+ apiKey: string;
46
+ baseURL: string;
47
+ modelName?: string;
48
+ logger?: Logger;
49
+ }
50
+
44
51
  export class AISDKExtractor implements StructuredExtractor {
45
52
  private logger: Logger;
46
53
  private model: any;
47
54
 
48
- constructor(logger?: Logger) {
49
- this.logger = logger || new Logger({ debug: false, prefix: '[AISDKExtractor]' });
50
-
51
- // Determine which provider to use based on environment variables
52
- // Priority: Anthropic (if ANTHROPIC_BASE_URL is set) > OpenAI
53
- const apiKey = process.env.ANTHROPIC_AUTH_TOKEN
54
- || process.env.ANTHROPIC_API_KEY
55
- || process.env.OPENAI_API_KEY;
55
+ constructor(config: StructuredExtractorConfig) {
56
+ this.logger = config.logger || new Logger({ debug: false, prefix: '[AISDKExtractor]' });
56
57
 
57
- if (!apiKey) {
58
+ if (!config.apiKey) {
58
59
  throw new Error('Missing API key for structured extraction. Ensure the LLM gateway is configured.');
59
60
  }
60
61
 
61
- const baseURL = process.env.ANTHROPIC_BASE_URL || process.env.OPENAI_BASE_URL;
62
- const modelName = 'claude-haiku-4-5';
63
- this.model = anthropic(modelName);
64
- this.logger.debug('Using Anthropic provider for structured extraction', { modelName, baseURL });
62
+ this.model = getAnthropicModel({
63
+ apiKey: config.apiKey,
64
+ baseURL: config.baseURL,
65
+ modelName: config.modelName || 'claude-haiku-4-5',
66
+ });
67
+
68
+ this.logger.debug('Using PostHog LLM gateway for structured extraction', {
69
+ modelName: config.modelName || 'claude-haiku-4-5',
70
+ baseURL: config.baseURL
71
+ });
65
72
  }
66
73
 
67
74
  async extractQuestions(researchContent: string): Promise<ExtractedQuestion[]> {
package/src/types.ts CHANGED
@@ -301,8 +301,8 @@ export interface AgentConfig {
301
301
  onEvent?: (event: AgentEvent) => void;
302
302
 
303
303
  // PostHog API configuration
304
- posthogApiUrl?: string;
305
- posthogApiKey?: string;
304
+ posthogApiUrl: string;
305
+ posthogApiKey: string;
306
306
 
307
307
  // PostHog MCP configuration
308
308
  posthogMcpUrl?: string;
@@ -0,0 +1,47 @@
1
+ import { createAnthropic } from '@ai-sdk/anthropic';
2
+
3
+ export interface PostHogGatewayConfig {
4
+ apiKey: string;
5
+ baseURL: string;
6
+ modelName?: string;
7
+ }
8
+
9
+ /**
10
+ * Creates an Anthropic model configured for PostHog LLM gateway.
11
+ *
12
+ * Handles two key differences between AI SDK and PostHog gateway:
13
+ * 1. Appends /v1 to baseURL (gateway expects /v1/messages, SDK appends /messages)
14
+ * 2. Converts x-api-key header to Authorization Bearer token
15
+ */
16
+ export function getAnthropicModel(config: PostHogGatewayConfig) {
17
+ const modelName = config.modelName || 'claude-haiku-4-5';
18
+
19
+ // PostHog gateway expects /v1/messages, but AI SDK appends /messages
20
+ // So we need to append /v1 to the baseURL
21
+ const baseURL = config.baseURL ? `${config.baseURL}/v1` : undefined;
22
+
23
+ // Custom fetch to convert x-api-key header to Authorization Bearer
24
+ // PostHog gateway expects Bearer token, but Anthropic SDK sends x-api-key
25
+ const customFetch = async (url: RequestInfo, init?: RequestInit): Promise<Response> => {
26
+ const headers = new Headers(init?.headers);
27
+
28
+ if (headers.has('x-api-key')) {
29
+ headers.delete('x-api-key');
30
+ headers.set('Authorization', `Bearer ${config.apiKey}`);
31
+ }
32
+
33
+ return fetch(url, {
34
+ ...init,
35
+ headers,
36
+ });
37
+ };
38
+
39
+ const anthropic = createAnthropic({
40
+ apiKey: config.apiKey,
41
+ baseURL,
42
+ //@ts-ignore
43
+ fetch: customFetch,
44
+ });
45
+
46
+ return anthropic(modelName);
47
+ }
@@ -79,6 +79,9 @@ export const buildStep: WorkflowStepRunner = async ({ step, context }) => {
79
79
  options: { ...baseOptions, ...(options.queryOverrides || {}) },
80
80
  });
81
81
 
82
+ // Track commits made during Claude Code execution
83
+ const commitTracker = await gitManager.trackCommitsDuring();
84
+
82
85
  for await (const message of response) {
83
86
  emitEvent(adapter.createRawSDKEvent(message));
84
87
  const transformed = adapter.transform(message);
@@ -87,29 +90,21 @@ export const buildStep: WorkflowStepRunner = async ({ step, context }) => {
87
90
  }
88
91
  }
89
92
 
90
- const hasChanges = await gitManager.hasChanges();
91
- context.stepResults[step.id] = { commitCreated: false };
92
- if (!hasChanges) {
93
- stepLogger.warn('No changes to commit in build phase', { taskId: task.id });
94
- emitEvent(adapter.createStatusEvent('phase_complete', { phase: 'build' }));
95
- return { status: 'completed' };
96
- }
97
-
98
- await gitManager.addFiles(['.']);
99
- const commitCreated = await finalizeStepGitActions(context, step, {
93
+ // Finalize: commit any remaining changes and optionally push
94
+ const { commitCreated, pushedBranch } = await commitTracker.finalize({
100
95
  commitMessage: `Implementation for ${task.title}`,
96
+ push: step.push,
101
97
  });
98
+
102
99
  context.stepResults[step.id] = { commitCreated };
103
100
 
104
101
  if (!commitCreated) {
105
- stepLogger.warn('No commit created during build step', { taskId: task.id });
106
- }
107
-
108
- // Always push after build if configured, even if agent created the commits
109
- if (step.push && !commitCreated) {
110
- const branchName = await gitManager.getCurrentBranch();
111
- await gitManager.pushBranch(branchName);
112
- stepLogger.info('Pushed branch after build', { branch: branchName });
102
+ stepLogger.warn('No changes to commit in build phase', { taskId: task.id });
103
+ } else {
104
+ stepLogger.info('Build commits finalized', {
105
+ taskId: task.id,
106
+ pushedBranch
107
+ });
113
108
  }
114
109
 
115
110
  emitEvent(adapter.createStatusEvent('phase_complete', { phase: 'build' }));