@posthog/agent 2.3.524 → 2.3.527

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": "2.3.524",
3
+ "version": "2.3.527",
4
4
  "repository": "https://github.com/PostHog/code",
5
5
  "description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
6
6
  "exports": {
@@ -20,6 +20,10 @@
20
20
  "types": "./dist/posthog-api.d.ts",
21
21
  "import": "./dist/posthog-api.js"
22
22
  },
23
+ "./pr-url-detector": {
24
+ "types": "./dist/pr-url-detector.d.ts",
25
+ "import": "./dist/pr-url-detector.js"
26
+ },
23
27
  "./types": {
24
28
  "types": "./dist/types.d.ts",
25
29
  "import": "./dist/types.js"
@@ -102,9 +106,9 @@
102
106
  "tsx": "^4.20.6",
103
107
  "typescript": "^5.5.0",
104
108
  "vitest": "^2.1.8",
105
- "@posthog/git": "1.0.0",
106
109
  "@posthog/shared": "1.0.0",
107
- "@posthog/enricher": "1.0.0"
110
+ "@posthog/enricher": "1.0.0",
111
+ "@posthog/git": "1.0.0"
108
112
  },
109
113
  "dependencies": {
110
114
  "@agentclientprotocol/sdk": "0.19.0",
@@ -90,13 +90,23 @@ function toolMeta(
90
90
  toolName: string,
91
91
  toolResponse?: unknown,
92
92
  parentToolCallId?: string,
93
+ bashCommand?: string,
93
94
  ): ToolUpdateMeta {
94
95
  const meta: ToolUpdateMeta["claudeCode"] = { toolName };
95
96
  if (toolResponse !== undefined) meta.toolResponse = toolResponse;
96
97
  if (parentToolCallId) meta.parentToolCallId = parentToolCallId;
98
+ if (bashCommand) meta.bashCommand = bashCommand;
97
99
  return { claudeCode: meta };
98
100
  }
99
101
 
102
+ function bashCommandFromToolUse(
103
+ toolUse: ToolUseCache[string] | undefined,
104
+ ): string | undefined {
105
+ if (!toolUse || toolUse.name !== "Bash") return undefined;
106
+ const command = (toolUse.input as { command?: unknown } | undefined)?.command;
107
+ return typeof command === "string" ? command : undefined;
108
+ }
109
+
100
110
  function handleTextChunk(
101
111
  chunk: { text: string },
102
112
  role: Role,
@@ -181,7 +191,12 @@ function handleToolUseChunk(
181
191
  await ctx.client.sessionUpdate({
182
192
  sessionId: ctx.sessionId,
183
193
  update: {
184
- _meta: toolMeta(toolUse.name, toolResponse, ctx.parentToolCallId),
194
+ _meta: toolMeta(
195
+ toolUse.name,
196
+ toolResponse,
197
+ ctx.parentToolCallId,
198
+ bashCommandFromToolUse(toolUse),
199
+ ),
185
200
  toolCallId: toolUseId,
186
201
  sessionUpdate: "tool_call_update",
187
202
  ...(editUpdate ? editUpdate : {}),
@@ -211,7 +226,12 @@ function handleToolUseChunk(
211
226
  });
212
227
 
213
228
  const meta: Record<string, unknown> = {
214
- ...toolMeta(chunk.name, undefined, ctx.parentToolCallId),
229
+ ...toolMeta(
230
+ chunk.name,
231
+ undefined,
232
+ ctx.parentToolCallId,
233
+ bashCommandFromToolUse(chunk),
234
+ ),
215
235
  };
216
236
  if (chunk.name === "Bash" && ctx.supportsTerminalOutput && !alreadyCached) {
217
237
  meta.terminal_info = { terminal_id: chunk.id };
@@ -351,7 +371,12 @@ function handleToolResultChunk(
351
371
  }
352
372
 
353
373
  const meta: Record<string, unknown> = {
354
- ...toolMeta(toolUse.name, undefined, ctx.parentToolCallId),
374
+ ...toolMeta(
375
+ toolUse.name,
376
+ undefined,
377
+ ctx.parentToolCallId,
378
+ bashCommandFromToolUse(toolUse),
379
+ ),
355
380
  ...(resultMeta?.terminal_exit
356
381
  ? { terminal_exit: resultMeta.terminal_exit }
357
382
  : {}),
@@ -106,6 +106,7 @@ export type ToolUpdateMeta = {
106
106
  toolName: string;
107
107
  toolResponse?: unknown;
108
108
  parentToolCallId?: string;
109
+ bashCommand?: string;
109
110
  };
110
111
  terminal_info?: TerminalInfo;
111
112
  terminal_output?: TerminalOutput;
@@ -300,7 +300,7 @@ export class HandoffCheckpointTracker {
300
300
  checkpoint: GitHandoffCheckpoint,
301
301
  uploads: Uploads,
302
302
  ): void {
303
- this.logger.info("Captured handoff checkpoint", {
303
+ this.logger.debug("Captured handoff checkpoint", {
304
304
  branch: checkpoint.branch,
305
305
  head: checkpoint.head?.slice(0, 7),
306
306
  totalBytes: this.sumRawBytes(uploads.pack, uploads.index),
@@ -312,7 +312,7 @@ export class HandoffCheckpointTracker {
312
312
  _downloads: Downloads,
313
313
  totalBytes: number,
314
314
  ): void {
315
- this.logger.info("Applied handoff checkpoint", {
315
+ this.logger.debug("Applied handoff checkpoint", {
316
316
  branch: checkpoint.branch,
317
317
  head: checkpoint.head?.slice(0, 7),
318
318
  totalBytes,
@@ -0,0 +1,140 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ type ExtractCreatedPrUrlInput,
4
+ extractCreatedPrUrl,
5
+ } from "./pr-url-detector";
6
+
7
+ const PR_URL = "https://github.com/PostHog/posthog/pull/12345";
8
+
9
+ interface Case {
10
+ name: string;
11
+ input: ExtractCreatedPrUrlInput;
12
+ expected: string | null;
13
+ }
14
+
15
+ const cases: Case[] = [
16
+ {
17
+ name: "returns the URL when gh pr create produced it (string toolResponse)",
18
+ input: {
19
+ toolName: "Bash",
20
+ bashCommand: 'gh pr create --title "x" --body "y"',
21
+ toolResponse: `${PR_URL}\n`,
22
+ },
23
+ expected: PR_URL,
24
+ },
25
+ {
26
+ name: "returns the URL when gh pr create produced it (object toolResponse)",
27
+ input: {
28
+ toolName: "Bash",
29
+ bashCommand: "gh pr create --fill",
30
+ toolResponse: { stdout: `${PR_URL}\n`, stderr: "" },
31
+ },
32
+ expected: PR_URL,
33
+ },
34
+ {
35
+ name: "ignores PR URLs from gh pr view",
36
+ input: {
37
+ toolName: "Bash",
38
+ bashCommand: `gh pr view ${PR_URL}`,
39
+ toolResponse: { stdout: PR_URL },
40
+ },
41
+ expected: null,
42
+ },
43
+ {
44
+ name: "ignores PR URLs from gh search prs",
45
+ input: {
46
+ toolName: "Bash",
47
+ bashCommand: 'gh search prs "fix login"',
48
+ toolResponse: PR_URL,
49
+ },
50
+ expected: null,
51
+ },
52
+ {
53
+ name: "ignores PR URLs from gh pr list",
54
+ input: {
55
+ toolName: "Bash",
56
+ bashCommand: "gh pr list --json url",
57
+ toolResponse: PR_URL,
58
+ },
59
+ expected: null,
60
+ },
61
+ {
62
+ name: "returns null when bashCommand is missing",
63
+ input: {
64
+ toolName: "Bash",
65
+ bashCommand: undefined,
66
+ toolResponse: PR_URL,
67
+ },
68
+ expected: null,
69
+ },
70
+ {
71
+ name: "returns null for non-Bash tools",
72
+ input: {
73
+ toolName: "Edit",
74
+ bashCommand: "gh pr create",
75
+ toolResponse: PR_URL,
76
+ },
77
+ expected: null,
78
+ },
79
+ {
80
+ name: "accepts the lowercase 'bash' tool variant",
81
+ input: {
82
+ toolName: "bash",
83
+ bashCommand: "gh pr create",
84
+ toolResponse: PR_URL,
85
+ },
86
+ expected: PR_URL,
87
+ },
88
+ {
89
+ name: "returns null when output has no PR URL",
90
+ input: {
91
+ toolName: "Bash",
92
+ bashCommand: "gh pr create",
93
+ toolResponse: "no pr was created",
94
+ },
95
+ expected: null,
96
+ },
97
+ {
98
+ name: "finds the URL in the content array when toolResponse is empty",
99
+ input: {
100
+ toolName: "Bash",
101
+ bashCommand: "gh pr create --fill",
102
+ toolResponse: undefined,
103
+ content: [{ type: "text", text: `Created: ${PR_URL}` }],
104
+ },
105
+ expected: PR_URL,
106
+ },
107
+ {
108
+ name: "handles output field on object toolResponse",
109
+ input: {
110
+ toolName: "Bash",
111
+ bashCommand: "gh pr create",
112
+ toolResponse: { output: PR_URL },
113
+ },
114
+ expected: PR_URL,
115
+ },
116
+ {
117
+ name: "matches gh pr create even with a chained command",
118
+ input: {
119
+ toolName: "Bash",
120
+ bashCommand: "git push -u origin feat/x && gh pr create --fill",
121
+ toolResponse: { stdout: PR_URL },
122
+ },
123
+ expected: PR_URL,
124
+ },
125
+ {
126
+ name: "does not match a fake command containing 'pr create' as text",
127
+ input: {
128
+ toolName: "Bash",
129
+ bashCommand: "echo 'i should pr create later'",
130
+ toolResponse: PR_URL,
131
+ },
132
+ expected: null,
133
+ },
134
+ ];
135
+
136
+ describe("extractCreatedPrUrl", () => {
137
+ it.each(cases)("$name", ({ input, expected }) => {
138
+ expect(extractCreatedPrUrl(input)).toBe(expected);
139
+ });
140
+ });
@@ -0,0 +1,46 @@
1
+ const PR_URL_REGEX = /https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/;
2
+ const GH_PR_CREATE_REGEX = /\bgh\s+pr\s+create\b/;
3
+
4
+ export interface ExtractCreatedPrUrlInput {
5
+ toolName: string | undefined;
6
+ bashCommand: string | undefined;
7
+ toolResponse: unknown;
8
+ content?: Array<{ type?: string; text?: string }>;
9
+ }
10
+
11
+ export function extractCreatedPrUrl(
12
+ input: ExtractCreatedPrUrlInput,
13
+ ): string | null {
14
+ const { toolName, bashCommand, toolResponse, content } = input;
15
+
16
+ if (!toolName || !/bash/i.test(toolName)) return null;
17
+ if (!bashCommand || !GH_PR_CREATE_REGEX.test(bashCommand)) return null;
18
+
19
+ let textToSearch = "";
20
+
21
+ if (toolResponse) {
22
+ if (typeof toolResponse === "string") {
23
+ textToSearch = toolResponse;
24
+ } else if (typeof toolResponse === "object" && toolResponse !== null) {
25
+ const respObj = toolResponse as Record<string, unknown>;
26
+ textToSearch =
27
+ String(respObj.stdout || "") + String(respObj.stderr || "");
28
+ if (!textToSearch && respObj.output) {
29
+ textToSearch = String(respObj.output);
30
+ }
31
+ }
32
+ }
33
+
34
+ if (Array.isArray(content)) {
35
+ for (const item of content) {
36
+ if (item.type === "text" && item.text) {
37
+ textToSearch += ` ${item.text}`;
38
+ }
39
+ }
40
+ }
41
+
42
+ if (!textToSearch) return null;
43
+
44
+ const match = textToSearch.match(PR_URL_REGEX);
45
+ return match ? match[0] : null;
46
+ }
@@ -625,7 +625,7 @@ describe("AgentServer HTTP Mode", () => {
625
625
  });
626
626
 
627
627
  describe("detectedPrUrl tracking", () => {
628
- it("stores PR URL when detectAndAttachPrUrl finds a match", () => {
628
+ it("stores PR URL when gh pr create produces it", () => {
629
629
  const s = createServer();
630
630
  const payload = {
631
631
  task_id: "test-task-id",
@@ -635,6 +635,7 @@ describe("AgentServer HTTP Mode", () => {
635
635
  _meta: {
636
636
  claudeCode: {
637
637
  toolName: "Bash",
638
+ bashCommand: 'gh pr create --title "x" --body "y"',
638
639
  toolResponse: {
639
640
  stdout:
640
641
  "https://github.com/PostHog/posthog/pull/42\nCreating pull request...",
@@ -659,6 +660,7 @@ describe("AgentServer HTTP Mode", () => {
659
660
  _meta: {
660
661
  claudeCode: {
661
662
  toolName: "Bash",
663
+ bashCommand: "gh pr create",
662
664
  toolResponse: { stdout: "just some output" },
663
665
  },
664
666
  },
@@ -667,6 +669,71 @@ describe("AgentServer HTTP Mode", () => {
667
669
  (s as unknown as TestableServer).detectAndAttachPrUrl(payload, update);
668
670
  expect((s as unknown as TestableServer).detectedPrUrl).toBeNull();
669
671
  });
672
+
673
+ it("does not attach PR URL when the bash command is gh pr view", () => {
674
+ const s = createServer();
675
+ const payload = {
676
+ task_id: "test-task-id",
677
+ run_id: "test-run-id",
678
+ };
679
+ const update = {
680
+ _meta: {
681
+ claudeCode: {
682
+ toolName: "Bash",
683
+ bashCommand: "gh pr view 42 --json url",
684
+ toolResponse: {
685
+ stdout: "https://github.com/PostHog/posthog/pull/42",
686
+ },
687
+ },
688
+ },
689
+ };
690
+
691
+ (s as unknown as TestableServer).detectAndAttachPrUrl(payload, update);
692
+ expect((s as unknown as TestableServer).detectedPrUrl).toBeNull();
693
+ });
694
+
695
+ it("does not attach PR URL when the bash command is gh search prs", () => {
696
+ const s = createServer();
697
+ const payload = {
698
+ task_id: "test-task-id",
699
+ run_id: "test-run-id",
700
+ };
701
+ const update = {
702
+ _meta: {
703
+ claudeCode: {
704
+ toolName: "Bash",
705
+ bashCommand: 'gh search prs "fix login"',
706
+ toolResponse: {
707
+ stdout: "https://github.com/PostHog/posthog/pull/42",
708
+ },
709
+ },
710
+ },
711
+ };
712
+
713
+ (s as unknown as TestableServer).detectAndAttachPrUrl(payload, update);
714
+ expect((s as unknown as TestableServer).detectedPrUrl).toBeNull();
715
+ });
716
+
717
+ it("does not attach PR URL when bashCommand is missing", () => {
718
+ const s = createServer();
719
+ const payload = {
720
+ task_id: "test-task-id",
721
+ run_id: "test-run-id",
722
+ };
723
+ const update = {
724
+ _meta: {
725
+ claudeCode: {
726
+ toolName: "Bash",
727
+ toolResponse: {
728
+ stdout: "https://github.com/PostHog/posthog/pull/42",
729
+ },
730
+ },
731
+ },
732
+ };
733
+
734
+ (s as unknown as TestableServer).detectAndAttachPrUrl(payload, update);
735
+ expect((s as unknown as TestableServer).detectedPrUrl).toBeNull();
736
+ });
670
737
  });
671
738
 
672
739
  describe("buildCloudSystemPrompt", () => {
@@ -29,6 +29,7 @@ import type { PermissionMode } from "../execution-mode";
29
29
  import { DEFAULT_CODEX_MODEL } from "../gateway-models";
30
30
  import { HandoffCheckpointTracker } from "../handoff-checkpoint";
31
31
  import { PostHogAPIClient } from "../posthog-api";
32
+ import { extractCreatedPrUrl } from "../pr-url-detector";
32
33
  import {
33
34
  formatConversationForResume,
34
35
  type ResumeState,
@@ -588,7 +589,7 @@ export class AgentServer {
588
589
  switch (method) {
589
590
  case POSTHOG_NOTIFICATIONS.USER_MESSAGE:
590
591
  case "user_message": {
591
- this.logger.info("Received user_message command", {
592
+ this.logger.debug("Received user_message command", {
592
593
  hasContent:
593
594
  typeof params.content === "string"
594
595
  ? params.content.trim().length > 0
@@ -608,7 +609,7 @@ export class AgentServer {
608
609
  if (prompt.length === 0) {
609
610
  throw new Error("User message cannot be empty");
610
611
  }
611
- this.logger.info("Built user_message prompt", {
612
+ this.logger.debug("Built user_message prompt", {
612
613
  blockTypes: prompt.map((block) => block.type),
613
614
  });
614
615
  const promptPreview = promptBlocksToText(prompt);
@@ -718,7 +719,7 @@ export class AgentServer {
718
719
  ? params.mcpServers
719
720
  : [];
720
721
 
721
- this.logger.info("Refresh session requested", {
722
+ this.logger.debug("Refresh session requested", {
722
723
  serverCount: mcpServers.length,
723
724
  });
724
725
 
@@ -1191,7 +1192,7 @@ export class AgentServer {
1191
1192
  this.resumeState.latestGitCheckpoint,
1192
1193
  );
1193
1194
  checkpointApplied = true;
1194
- this.logger.info("Git checkpoint applied", {
1195
+ this.logger.debug("Git checkpoint applied", {
1195
1196
  branch: this.resumeState.latestGitCheckpoint.branch,
1196
1197
  head: this.resumeState.latestGitCheckpoint.head,
1197
1198
  packBytes: metrics.packBytes,
@@ -1314,7 +1315,7 @@ export class AgentServer {
1314
1315
  taskId: taskRun.task,
1315
1316
  runId: taskRun.id,
1316
1317
  });
1317
- this.logger.info("Built pending user prompt", {
1318
+ this.logger.debug("Built pending user prompt", {
1318
1319
  hasMessage: typeof message === "string" && message.trim().length > 0,
1319
1320
  requestedArtifactCount: artifactIds.length,
1320
1321
  blockTypes: prompt.map((block) => block.type),
@@ -2131,45 +2132,21 @@ ${attributionInstructions}
2131
2132
  const meta = (update?._meta as Record<string, unknown>)?.claudeCode as
2132
2133
  | Record<string, unknown>
2133
2134
  | undefined;
2134
- const toolResponse = meta?.toolResponse;
2135
-
2136
- // Extract text content from tool response
2137
- let textToSearch = "";
2138
-
2139
- if (toolResponse) {
2140
- if (typeof toolResponse === "string") {
2141
- textToSearch = toolResponse;
2142
- } else if (typeof toolResponse === "object" && toolResponse !== null) {
2143
- const respObj = toolResponse as Record<string, unknown>;
2144
- textToSearch =
2145
- String(respObj.stdout || "") + String(respObj.stderr || "");
2146
- if (!textToSearch && respObj.output) {
2147
- textToSearch = String(respObj.output);
2148
- }
2149
- }
2150
- }
2151
-
2152
- // Also check content array
2153
- const content = update?.content;
2154
- if (Array.isArray(content)) {
2155
- for (const item of content) {
2156
- if (item.type === "text" && item.text) {
2157
- textToSearch += ` ${item.text}`;
2158
- }
2159
- }
2160
- }
2161
2135
 
2162
- if (!textToSearch) return;
2136
+ const content = update?.content as
2137
+ | Array<{ type?: string; text?: string }>
2138
+ | undefined;
2163
2139
 
2164
- // Match GitHub PR URLs
2165
- const prUrlMatch = textToSearch.match(
2166
- /https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/,
2167
- );
2168
- if (!prUrlMatch) return;
2140
+ const prUrl = extractCreatedPrUrl({
2141
+ toolName: meta?.toolName as string | undefined,
2142
+ bashCommand: meta?.bashCommand as string | undefined,
2143
+ toolResponse: meta?.toolResponse,
2144
+ content,
2145
+ });
2146
+ if (!prUrl) return;
2169
2147
 
2170
- const prUrl = prUrlMatch[0];
2171
2148
  this.detectedPrUrl = prUrl;
2172
- this.logger.debug("Detected PR URL in bash output", {
2149
+ this.logger.debug("Detected PR URL from gh pr create", {
2173
2150
  runId: payload.run_id,
2174
2151
  prUrl,
2175
2152
  });