@posthog/agent 1.28.0 → 1.30.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/agent",
3
- "version": "1.28.0",
3
+ "version": "1.30.0",
4
4
  "repository": "https://github.com/PostHog/array",
5
5
  "description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
6
6
  "main": "./dist/index.js",
@@ -33,6 +33,8 @@ export const POSTHOG_NOTIFICATIONS = {
33
33
  CONSOLE: "_posthog/console",
34
34
  /** SDK session ID notification (for resumption) */
35
35
  SDK_SESSION: "_posthog/sdk_session",
36
+ /** Sandbox execution output (stdout/stderr from Modal or Docker) */
37
+ SANDBOX_OUTPUT: "_posthog/sandbox_output",
36
38
  } as const;
37
39
 
38
40
  export type PostHogNotificationType =
@@ -105,6 +107,16 @@ export interface SdkSessionPayload {
105
107
  sdkSessionId: string;
106
108
  }
107
109
 
110
+ /**
111
+ * Sandbox execution output
112
+ */
113
+ export interface SandboxOutputPayload {
114
+ sessionId: string;
115
+ stdout: string;
116
+ stderr: string;
117
+ exitCode: number;
118
+ }
119
+
108
120
  export type PostHogNotificationPayload =
109
121
  | ArtifactNotificationPayload
110
122
  | PhaseNotificationPayload
@@ -114,4 +126,5 @@ export type PostHogNotificationPayload =
114
126
  | TaskCompletePayload
115
127
  | ErrorNotificationPayload
116
128
  | ConsoleNotificationPayload
117
- | SdkSessionPayload;
129
+ | SdkSessionPayload
130
+ | SandboxOutputPayload;
@@ -135,6 +135,8 @@ export type NewSessionMeta = {
135
135
  */
136
136
  options?: Options;
137
137
  };
138
+ /** Initial model to use for the session (e.g., 'claude-opus-4-5', 'gpt-5.1') */
139
+ model?: string;
138
140
  };
139
141
 
140
142
  /**
@@ -443,6 +445,20 @@ export class ClaudeAcpAgent implements Agent {
443
445
  const availableCommands = await getAvailableSlashCommands(q);
444
446
  const models = await getAvailableModels(q);
445
447
 
448
+ // Set initial model if provided via _meta (must be after getAvailableModels which resets to default)
449
+ const requestedModel = (params._meta as NewSessionMeta | undefined)?.model;
450
+ if (requestedModel) {
451
+ try {
452
+ await q.setModel(requestedModel);
453
+ this.logger.info("Set initial model", { model: requestedModel });
454
+ } catch (err) {
455
+ this.logger.warn("Failed to set initial model, using default", {
456
+ requestedModel,
457
+ error: err,
458
+ });
459
+ }
460
+ }
461
+
446
462
  // Needs to happen after we return the session
447
463
  setTimeout(() => {
448
464
  this.client.sessionUpdate({
@@ -657,12 +673,16 @@ export class ClaudeAcpAgent implements Agent {
657
673
  throw RequestError.authRequired();
658
674
  }
659
675
 
660
- // For assistant messages, text/thinking are normally streamed via stream_event.
661
- // But some gateways (like LiteLLM) don't stream, so we process all content.
676
+ // Text/thinking is streamed via stream_event, so skip them here to avoid duplication.
662
677
  const content = message.message.content;
678
+ const contentToProcess = Array.isArray(content)
679
+ ? content.filter(
680
+ (block) => block.type !== "text" && block.type !== "thinking",
681
+ )
682
+ : content;
663
683
 
664
684
  for (const notification of toAcpNotifications(
665
- content,
685
+ contentToProcess as typeof content,
666
686
  message.message.role,
667
687
  params.sessionId,
668
688
  this.toolUseCache,
@@ -922,6 +942,15 @@ export class ClaudeAcpAgent implements Agent {
922
942
  return {};
923
943
  }
924
944
 
945
+ if (method === "session/setModel") {
946
+ const { sessionId, modelId } = params as {
947
+ sessionId: string;
948
+ modelId: string;
949
+ };
950
+ await this.setSessionModel({ sessionId, modelId });
951
+ return {};
952
+ }
953
+
925
954
  throw RequestError.methodNotFound(method);
926
955
  }
927
956
 
package/src/agent.ts CHANGED
@@ -57,8 +57,8 @@ export class Agent {
57
57
 
58
58
  // Add auth if API key provided
59
59
  const headers: Record<string, string> = {};
60
- if (config.posthogApiKey) {
61
- headers.Authorization = `Bearer ${config.posthogApiKey}`;
60
+ if (config.getPosthogApiKey) {
61
+ headers.Authorization = `Bearer ${config.getPosthogApiKey()}`;
62
62
  }
63
63
 
64
64
  const defaultMcpServers = {
@@ -93,12 +93,12 @@ export class Agent {
93
93
 
94
94
  if (
95
95
  config.posthogApiUrl &&
96
- config.posthogApiKey &&
96
+ config.getPosthogApiKey &&
97
97
  config.posthogProjectId
98
98
  ) {
99
99
  this.posthogAPI = new PostHogAPIClient({
100
100
  apiUrl: config.posthogApiUrl,
101
- apiKey: config.posthogApiKey,
101
+ getApiKey: config.getPosthogApiKey,
102
102
  projectId: config.posthogProjectId,
103
103
  });
104
104
 
@@ -126,7 +126,7 @@ export class Agent {
126
126
  }
127
127
 
128
128
  /**
129
- * Configure LLM gateway environment variables for Claude Code CLI
129
+ * Configure LLM gateway environment variables for Claude Code CLI.
130
130
  */
131
131
  private async _configureLlmGateway(): Promise<void> {
132
132
  if (!this.posthogAPI) {
@@ -139,6 +139,7 @@ export class Agent {
139
139
  process.env.ANTHROPIC_BASE_URL = gatewayUrl;
140
140
  process.env.ANTHROPIC_AUTH_TOKEN = apiKey;
141
141
  this.ensureOpenAIGatewayEnv(gatewayUrl, apiKey);
142
+ this.ensureGeminiGatewayEnv(gatewayUrl, apiKey);
142
143
  } catch (error) {
143
144
  this.logger.error("Failed to configure LLM gateway", error);
144
145
  throw error;
@@ -373,9 +374,7 @@ export class Agent {
373
374
  **Description**: ${taskDescription}
374
375
 
375
376
  ## Changes
376
- This PR implements the changes described in the task.
377
-
378
- Generated by PostHog Agent`;
377
+ This PR implements the changes described in the task.`;
379
378
  const prBody = customBody || defaultBody;
380
379
 
381
380
  const prUrl = await this.gitManager.createPullRequest(
@@ -529,6 +528,19 @@ Generated by PostHog Agent`;
529
528
  }
530
529
  }
531
530
 
531
+ private ensureGeminiGatewayEnv(gatewayUrl?: string, token?: string): void {
532
+ const resolvedGatewayUrl = gatewayUrl || process.env.ANTHROPIC_BASE_URL;
533
+ const resolvedToken = token || process.env.ANTHROPIC_AUTH_TOKEN;
534
+
535
+ if (resolvedGatewayUrl) {
536
+ process.env.GEMINI_BASE_URL = resolvedGatewayUrl;
537
+ }
538
+
539
+ if (resolvedToken) {
540
+ process.env.GEMINI_API_KEY = resolvedToken;
541
+ }
542
+ }
543
+
532
544
  async runTaskCloud(
533
545
  taskId: string,
534
546
  taskRunId: string,
@@ -6,8 +6,6 @@ const execAsync = promisify(exec);
6
6
 
7
7
  export interface GitConfig {
8
8
  repositoryPath: string;
9
- authorName?: string;
10
- authorEmail?: string;
11
9
  logger?: Logger;
12
10
  }
13
11
 
@@ -19,14 +17,10 @@ export interface BranchInfo {
19
17
 
20
18
  export class GitManager {
21
19
  private repositoryPath: string;
22
- private authorName?: string;
23
- private authorEmail?: string;
24
20
  private logger: Logger;
25
21
 
26
22
  constructor(config: GitConfig) {
27
23
  this.repositoryPath = config.repositoryPath;
28
- this.authorName = config.authorName;
29
- this.authorEmail = config.authorEmail;
30
24
  this.logger =
31
25
  config.logger || new Logger({ debug: false, prefix: "[GitManager]" });
32
26
  }
@@ -170,8 +164,7 @@ export class GitManager {
170
164
  async commitChanges(
171
165
  message: string,
172
166
  options?: {
173
- authorName?: string;
174
- authorEmail?: string;
167
+ allowEmpty?: boolean;
175
168
  },
176
169
  ): Promise<string> {
177
170
  const command = this.buildCommitCommand(message, options);
@@ -244,8 +237,6 @@ export class GitManager {
244
237
  message: string,
245
238
  options?: {
246
239
  allowEmpty?: boolean;
247
- authorName?: string;
248
- authorEmail?: string;
249
240
  },
250
241
  ): string {
251
242
  let command = `commit -m "${this.escapeShellArg(message)}"`;
@@ -254,13 +245,6 @@ export class GitManager {
254
245
  command += " --allow-empty";
255
246
  }
256
247
 
257
- const authorName = options?.authorName || this.authorName;
258
- const authorEmail = options?.authorEmail || this.authorEmail;
259
-
260
- if (authorName && authorEmail) {
261
- command += ` --author="${authorName} <${authorEmail}>"`;
262
- }
263
-
264
248
  return command;
265
249
  }
266
250
 
@@ -427,7 +411,6 @@ export class GitManager {
427
411
  const message = `📋 Add plan for task: ${taskTitle}
428
412
 
429
413
  Task ID: ${taskId}
430
- Generated by PostHog Agent
431
414
 
432
415
  This commit contains the implementation plan and supporting documentation
433
416
  for the task. Review the plan before proceeding with implementation.`;
@@ -452,8 +435,7 @@ for the task. Review the plan before proceeding with implementation.`;
452
435
 
453
436
  let message = `✨ Implement task: ${taskTitle}
454
437
 
455
- Task ID: ${taskId}
456
- Generated by PostHog Agent`;
438
+ Task ID: ${taskId}`;
457
439
 
458
440
  if (planSummary) {
459
441
  message += `\n\nPlan Summary:\n${planSummary}`;
@@ -8,6 +8,7 @@ import type {
8
8
  TaskRunArtifact,
9
9
  UrlMention,
10
10
  } from "./types.js";
11
+ import { getLlmGatewayUrl } from "./utils/gateway.js";
11
12
 
12
13
  interface PostHogApiResponse<T> {
13
14
  results?: T[];
@@ -42,7 +43,7 @@ export class PostHogAPIClient {
42
43
 
43
44
  private get headers(): Record<string, string> {
44
45
  return {
45
- Authorization: `Bearer ${this.config.apiKey}`,
46
+ Authorization: `Bearer ${this.config.getApiKey()}`,
46
47
  "Content-Type": "application/json",
47
48
  };
48
49
  }
@@ -84,12 +85,11 @@ export class PostHogAPIClient {
84
85
  }
85
86
 
86
87
  getApiKey(): string {
87
- return this.config.apiKey;
88
+ return this.config.getApiKey();
88
89
  }
89
90
 
90
91
  getLlmGatewayUrl(): string {
91
- const teamId = this.getTeamId();
92
- return `${this.baseUrl}/api/projects/${teamId}/llm_gateway`;
92
+ return getLlmGatewayUrl(this.baseUrl);
93
93
  }
94
94
 
95
95
  async fetchTask(taskId: string): Promise<Task> {
@@ -215,17 +215,19 @@ export class PromptBuilder {
215
215
  return { description, referencedFiles };
216
216
  }
217
217
 
218
- // Read all referenced files
218
+ // Read all referenced files, tracking which ones succeed
219
+ const successfulPaths = new Set<string>();
219
220
  for (const filePath of filePaths) {
220
221
  const content = await this.readFileContent(repositoryPath, filePath);
221
222
  if (content !== null) {
222
223
  referencedFiles.push({ path: filePath, content });
224
+ successfulPaths.add(filePath);
223
225
  }
224
226
  }
225
227
 
226
- // Replace file tags with just the filename for readability
228
+ // Only replace tags for files that were successfully read
227
229
  let processedDescription = description;
228
- for (const filePath of filePaths) {
230
+ for (const filePath of successfulPaths) {
229
231
  const fileName = filePath.split("/").pop() || filePath;
230
232
  processedDescription = processedDescription.replace(
231
233
  new RegExp(
@@ -205,7 +205,7 @@ export class TemplateManager {
205
205
  generatePostHogReadme(): string {
206
206
  return `# PostHog Task Files
207
207
 
208
- This directory contains task-related files generated by the PostHog Agent.
208
+ This directory contains task-related files.
209
209
 
210
210
  ## Structure
211
211
 
@@ -221,7 +221,7 @@ Each task has its own subdirectory: \`.posthog/{task-id}/\`
221
221
 
222
222
  These files are:
223
223
  - Version controlled alongside your code
224
- - Used by the PostHog Agent for context
224
+ - Used for task context and planning
225
225
  - Available for review in pull requests
226
226
  - Organized by task ID for easy reference
227
227
 
@@ -231,10 +231,6 @@ Customize \`.posthog/.gitignore\` to control which files are committed:
231
231
  - Include plans and documentation by default
232
232
  - Exclude temporary files and sensitive data
233
233
  - Customize based on your team's needs
234
-
235
- ---
236
-
237
- *Generated by PostHog Agent*
238
234
  `;
239
235
  }
240
236
  }
package/src/types.ts CHANGED
@@ -197,7 +197,7 @@ export interface AgentConfig {
197
197
 
198
198
  // PostHog API configuration (optional - enables PostHog integration when provided)
199
199
  posthogApiUrl?: string;
200
- posthogApiKey?: string;
200
+ getPosthogApiKey?: () => string;
201
201
  posthogProjectId?: number;
202
202
 
203
203
  // PostHog MCP configuration
@@ -219,7 +219,7 @@ export interface AgentConfig {
219
219
 
220
220
  export interface PostHogAPIConfig {
221
221
  apiUrl: string;
222
- apiKey: string;
222
+ getApiKey: () => string;
223
223
  projectId: number;
224
224
  }
225
225
 
@@ -0,0 +1,15 @@
1
+ export function getLlmGatewayUrl(posthogHost: string): string {
2
+ const url = new URL(posthogHost);
3
+ const hostname = url.hostname;
4
+
5
+ if (hostname === "localhost" || hostname === "127.0.0.1") {
6
+ return `${url.protocol}//localhost:3308`;
7
+ }
8
+
9
+ // Extract region from hostname (us.posthog.com, eu.posthog.com)
10
+ // app.posthog.com is legacy US
11
+ const regionMatch = hostname.match(/^(us|eu)\.posthog\.com$/);
12
+ const region = regionMatch ? regionMatch[1] : "us";
13
+
14
+ return `https://gateway.${region}.posthog.com`;
15
+ }
@@ -628,7 +628,9 @@ export class WorktreeManager {
628
628
  }
629
629
  }
630
630
 
631
- async createWorktree(): Promise<WorktreeInfo> {
631
+ async createWorktree(options?: {
632
+ baseBranch?: string;
633
+ }): Promise<WorktreeInfo> {
632
634
  // Only modify .git/info/exclude when using in-repo storage
633
635
  if (!this.usesExternalPath()) {
634
636
  await this.ensureArrayDirIgnored();
@@ -644,7 +646,7 @@ export class WorktreeManager {
644
646
  const worktreeName = await this.generateUniqueWorktreeName();
645
647
  const worktreePath = this.getWorktreePath(worktreeName);
646
648
  const branchName = `array/${worktreeName}`;
647
- const baseBranch = await this.getDefaultBranch();
649
+ const baseBranch = options?.baseBranch ?? (await this.getDefaultBranch());
648
650
 
649
651
  this.logger.info("Creating worktree", {
650
652
  worktreeName,