@posthog/agent 2.3.616 → 2.3.643

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.616",
3
+ "version": "2.3.643",
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": {
@@ -106,9 +106,9 @@
106
106
  "tsx": "^4.20.6",
107
107
  "typescript": "^5.5.0",
108
108
  "vitest": "^2.1.8",
109
+ "@posthog/git": "1.0.0",
109
110
  "@posthog/shared": "1.0.0",
110
- "@posthog/enricher": "1.0.0",
111
- "@posthog/git": "1.0.0"
111
+ "@posthog/enricher": "1.0.0"
112
112
  },
113
113
  "dependencies": {
114
114
  "@agentclientprotocol/sdk": "0.19.0",
@@ -1,8 +1,34 @@
1
+ import type { PromptRequest } from "@agentclientprotocol/sdk";
1
2
  import { describe, expect, it } from "vitest";
2
- import { promptToClaude } from "./acp-to-sdk";
3
+ import {
4
+ promptToClaude,
5
+ readToolGuidanceForPath,
6
+ workspacePromptFromFileUri,
7
+ } from "./acp-to-sdk";
8
+
9
+ describe("readToolGuidanceForPath", () => {
10
+ it.each([
11
+ ["/docs/x.pdf", ["pages"]],
12
+ ["/proj/app.ts", ["offset", "limit"]],
13
+ ["/assets/logo.png", ["Binary", "file_path"]],
14
+ ])("guides reads for %s", (filePath, keywords) => {
15
+ const guidance = readToolGuidanceForPath(filePath);
16
+ for (const keyword of keywords) {
17
+ expect(guidance).toContain(keyword);
18
+ }
19
+ });
20
+ });
21
+
22
+ describe("workspacePromptFromFileUri", () => {
23
+ it("includes file_path and Read-oriented chunking hints", () => {
24
+ const s = workspacePromptFromFileUri("file:///tmp/x.pdf");
25
+ expect(s).toContain("Read");
26
+ expect(s).toContain("file_path: /tmp/x.pdf");
27
+ });
28
+ });
3
29
 
4
30
  describe("promptToClaude", () => {
5
- it("renders file resource links as explicit workspace attachments", () => {
31
+ it("maps file resource_link to workspace path + Read guidance", () => {
6
32
  const result = promptToClaude({
7
33
  sessionId: "session-1",
8
34
  prompt: [
@@ -14,17 +40,87 @@ describe("promptToClaude", () => {
14
40
  ],
15
41
  });
16
42
 
17
- expect(result.message.content).toEqual([
18
- {
19
- type: "text",
20
- text: [
21
- "Attached file available in the workspace:",
22
- "- name: report.pdf",
23
- "- path: /tmp/workspace/.posthog/attachments/run-1/report.pdf",
24
- "Use the available tools to inspect this file if needed.",
25
- ].join("\n"),
26
- },
27
- ]);
43
+ expect(result.message.content.length).toBe(1);
44
+ expect(result.message.content[0]).toMatchObject({
45
+ type: "text",
46
+ text: expect.any(String),
47
+ });
48
+ expect(
49
+ (result.message.content[0] as { type: string; text: string }).text,
50
+ ).toContain("file_path:");
51
+ expect(
52
+ (result.message.content[0] as { type: string; text: string }).text,
53
+ ).toContain("/tmp/workspace/.posthog/attachments/run-1/report.pdf");
54
+ const text = (result.message.content[0] as { text: string }).text;
55
+ expect(text.toLowerCase()).toContain("read");
56
+ expect(text).toContain("pages");
57
+ });
58
+
59
+ it("drops embedded body for file:// resource but keeps attachment:// payload", () => {
60
+ const hugeInline = `${"y".repeat(30_000)}KEEP_ATTACH${"y".repeat(30_000)}`;
61
+ const fileRes = promptToClaude({
62
+ sessionId: "x",
63
+ prompt: [
64
+ {
65
+ type: "resource",
66
+ resource: {
67
+ uri: "file:///tmp/note.txt",
68
+ text: `${"x".repeat(50_000)}DROP_THIS${"x".repeat(50_000)}`,
69
+ mimeType: "text/plain",
70
+ },
71
+ },
72
+ ],
73
+ });
74
+ expect(fileRes.message.content.length).toBe(1);
75
+ expect(JSON.stringify(fileRes)).not.toContain("DROP_THIS");
76
+ expect(fileRes.message.content[0]).toMatchObject({
77
+ type: "text",
78
+ text: expect.stringContaining("file_path: /tmp/note.txt"),
79
+ });
80
+
81
+ const attachRes = promptToClaude({
82
+ sessionId: "y",
83
+ prompt: [
84
+ {
85
+ type: "resource",
86
+ resource: {
87
+ uri: "attachment://z?label=f.txt",
88
+ text: hugeInline,
89
+ mimeType: "text/plain",
90
+ },
91
+ },
92
+ ],
93
+ });
94
+ expect(attachRes.message.content.length).toBe(2);
95
+ expect(JSON.stringify(attachRes)).toContain("KEEP_ATTACH");
96
+ });
97
+
98
+ it("maps file URI-only image blocks to workspace Read prompt text", () => {
99
+ const req: PromptRequest = {
100
+ sessionId: "session-1",
101
+ prompt: [
102
+ {
103
+ type: "image",
104
+ uri: "file:///tmp/ui/screenshot.png",
105
+ mimeType: "image/png",
106
+ } as PromptRequest["prompt"][number],
107
+ ],
108
+ };
109
+ const result = promptToClaude(req);
110
+
111
+ expect(result.message.content).toHaveLength(1);
112
+ expect(result.message.content[0]).toMatchObject({
113
+ type: "text",
114
+ text: expect.any(String),
115
+ });
116
+ expect(
117
+ (result.message.content[0] as { type: string; text: string }).text,
118
+ ).toContain("/tmp/ui/screenshot.png");
119
+ expect(
120
+ (
121
+ result.message.content[0] as { type: string; text: string }
122
+ ).text.toLowerCase(),
123
+ ).toContain("read");
28
124
  });
29
125
 
30
126
  it("preserves non-file resource links as links", () => {
@@ -6,6 +6,31 @@ import type { ContentBlockParam } from "@anthropic-ai/sdk/resources";
6
6
 
7
7
  type ImageMimeType = "image/jpeg" | "image/png" | "image/gif" | "image/webp";
8
8
 
9
+ const PDF_EXTENSIONS = new Set(["pdf"]);
10
+
11
+ const COMMON_IMAGE_EXTENSIONS = new Set([
12
+ "png",
13
+ "jpg",
14
+ "jpeg",
15
+ "gif",
16
+ "webp",
17
+ "bmp",
18
+ "svg",
19
+ "heic",
20
+ "tif",
21
+ "tiff",
22
+ ]);
23
+
24
+ const VIDEO_EXTENSIONS = new Set([
25
+ "mp4",
26
+ "mov",
27
+ "webm",
28
+ "mkv",
29
+ "avi",
30
+ "mpeg",
31
+ "mpg",
32
+ ]);
33
+
9
34
  function sdkText(value: string): ContentBlockParam {
10
35
  return { type: "text", text: value };
11
36
  }
@@ -22,21 +47,42 @@ function formatUriAsLink(uri: string): string {
22
47
  }
23
48
  }
24
49
 
25
- function formatFileAttachment(uri: string): string {
50
+ /** Chunking hints for Claude Code `Read` (`file_path`, optional `pages` / `offset` / `limit`). */
51
+ export function readToolGuidanceForPath(filePath: string): string {
52
+ const ext = path.extname(filePath).slice(1).toLowerCase();
53
+ if (PDF_EXTENSIONS.has(ext)) {
54
+ return 'Optional `pages` string (e.g. "1-5") per Read call instead of loading the entire PDF.';
55
+ }
56
+ if (COMMON_IMAGE_EXTENSIONS.has(ext) || VIDEO_EXTENSIONS.has(ext)) {
57
+ return "Binary file — use Read with `file_path`; prefer bounded reads where supported.";
58
+ }
59
+ return "Large text — use multiple Read calls with optional `offset` and `limit`.";
60
+ }
61
+
62
+ /** Path-only workspace attach text (never embed `resource.text` from disk). */
63
+ export function workspacePromptFromFileUri(uri: string): string {
26
64
  try {
27
65
  const filePath = fileURLToPath(uri);
28
66
  const name = path.basename(filePath) || filePath;
29
67
  return [
30
- "Attached file available in the workspace:",
31
- `- name: ${name}`,
32
- `- path: ${filePath}`,
33
- "Use the available tools to inspect this file if needed.",
68
+ "Attached workspace file use Read with required `file_path`:",
69
+ `- file_path: ${filePath}`,
70
+ `- name (context): ${name}`,
71
+ readToolGuidanceForPath(filePath),
34
72
  ].join("\n");
35
73
  } catch {
36
- return `Attached file available at ${uri}`;
74
+ return [
75
+ "Attached file — decode path from URI, call Read with that path as `file_path`:",
76
+ uri,
77
+ 'Chunk PDFs with `pages` (e.g. "1-5"); long text with `offset`/`limit`.',
78
+ ].join("\n");
37
79
  }
38
80
  }
39
81
 
82
+ function isFileSchemeUri(uri: string | undefined | null): boolean {
83
+ return Boolean(uri?.startsWith("file://"));
84
+ }
85
+
40
86
  function transformMcpCommand(text: string): string {
41
87
  const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(\s+.*)?$/);
42
88
  if (mcpMatch) {
@@ -60,7 +106,7 @@ function processPromptChunk(
60
106
  content.push(
61
107
  sdkText(
62
108
  chunk.uri.startsWith("file://")
63
- ? formatFileAttachment(chunk.uri)
109
+ ? workspacePromptFromFileUri(chunk.uri)
64
110
  : formatUriAsLink(chunk.uri),
65
111
  ),
66
112
  );
@@ -68,10 +114,16 @@ function processPromptChunk(
68
114
 
69
115
  case "resource":
70
116
  if ("text" in chunk.resource) {
71
- content.push(sdkText(formatUriAsLink(chunk.resource.uri)));
117
+ const uri = chunk.resource.uri;
118
+ if (uri != null && isFileSchemeUri(uri)) {
119
+ content.push(sdkText(workspacePromptFromFileUri(uri)));
120
+ break;
121
+ }
122
+
123
+ content.push(sdkText(formatUriAsLink(uri ?? "")));
72
124
  context.push(
73
125
  sdkText(
74
- `\n<context ref="${chunk.resource.uri}">\n${chunk.resource.text}\n</context>`,
126
+ `\n<context ref="${uri ?? ""}">\n${chunk.resource.text}\n</context>`,
75
127
  ),
76
128
  );
77
129
  }
@@ -92,6 +144,8 @@ function processPromptChunk(
92
144
  type: "image",
93
145
  source: { type: "url", url: chunk.uri },
94
146
  });
147
+ } else if (chunk.uri != null && isFileSchemeUri(chunk.uri)) {
148
+ content.push(sdkText(workspacePromptFromFileUri(chunk.uri)));
95
149
  }
96
150
  break;
97
151
 
@@ -826,6 +826,9 @@ describe("AgentServer HTTP Mode", () => {
826
826
  "If the user explicitly asks you to open or update a pull request",
827
827
  "open a draft pull request",
828
828
  "unless the user explicitly asks",
829
+ ".github/pull_request_template.md",
830
+ "gh issue list --search",
831
+ "Closes #<n>",
829
832
  "Generated-By: PostHog Code",
830
833
  "Task-Id: test-task-id",
831
834
  ],
@@ -868,6 +871,13 @@ describe("AgentServer HTTP Mode", () => {
868
871
  expect(prompt).toContain("Generated-By: PostHog Code");
869
872
  expect(prompt).toContain("Task-Id: test-task-id");
870
873
  expect(prompt).toContain("Created with [PostHog Code]");
874
+ // PR template detection (repo first, org `.github` fallback)
875
+ expect(prompt).toContain(".github/pull_request_template.md");
876
+ expect(prompt).toContain("org's `.github` repo");
877
+ // Related-issue linking
878
+ expect(prompt).toContain("gh issue list --state open --search");
879
+ expect(prompt).toContain("Closes #<n>");
880
+ expect(prompt).toContain("Refs #<n>");
871
881
  delete process.env.POSTHOG_CODE_INTERACTION_ORIGIN;
872
882
  });
873
883
 
@@ -895,6 +905,13 @@ describe("AgentServer HTTP Mode", () => {
895
905
  );
896
906
  expect(prompt).toContain("Push to the existing PR branch");
897
907
  expect(prompt).not.toContain("Create a draft pull request");
908
+ // Review-comment thread handling: reply + resolve
909
+ expect(prompt).toContain("review thread");
910
+ expect(prompt).toContain("/pulls/{n}/comments/{id}/replies");
911
+ expect(prompt).toContain("resolveReviewThread");
912
+ expect(prompt).toContain(
913
+ "Do NOT push fixes for review comments without replying to and resolving each related thread.",
914
+ );
898
915
  delete process.env.POSTHOG_CODE_INTERACTION_ORIGIN;
899
916
  });
900
917
 
@@ -1643,9 +1643,14 @@ After completing the requested changes:
1643
1643
  1. Check out the existing PR branch with \`gh pr checkout ${prUrl}\`
1644
1644
  2. Stage and commit all changes with a clear commit message
1645
1645
  3. Push to the existing PR branch
1646
+ 4. For every PR review comment or review thread you addressed, treat the thread as done only after BOTH of these:
1647
+ - Reply on the thread with a short note describing what changed (reference the commit SHA when useful) using \`gh api -X POST /repos/{owner}/{repo}/pulls/{n}/comments/{id}/replies -f body='...'\`.
1648
+ - Resolve the thread via the \`resolveReviewThread\` GraphQL mutation: \`gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -f id="<thread-node-id>"\`.
1649
+ List unresolved threads first with \`gh api graphql -f query='{repository(owner:"<owner>",name:"<repo>"){pullRequest(number:<n>){reviewThreads(first:100){nodes{id isResolved comments(first:1){nodes{body}}}}}}}'\` so you can resolve each one you fixed.
1646
1650
 
1647
1651
  Important:
1648
1652
  - Do NOT create a new branch or a new pull request.
1653
+ - Do NOT push fixes for review comments without replying to and resolving each related thread.
1649
1654
  ${attributionInstructions}
1650
1655
  `;
1651
1656
  }
@@ -1661,7 +1666,7 @@ When the user asks for code changes:
1661
1666
  When the user explicitly asks to clone or work in a GitHub repository:
1662
1667
  - Clone the repository into /tmp/workspace/repos/<owner>/<repo> using \`gh repo clone <owner>/<repo> /tmp/workspace/repos/<owner>/<repo>\`
1663
1668
  - Work from inside that cloned repository for follow-up code changes
1664
- - If the user explicitly asks you to open or update a pull request, create a branch, commit the requested changes, push it, and open a draft pull request from inside the clone
1669
+ - If the user explicitly asks you to open or update a pull request, create a branch, commit the requested changes, push it, and open a draft pull request from inside the clone. Before opening the PR, check the cloned repo for a PR template at \`.github/pull_request_template.md\` (or variants; fall back to the org's \`.github\` repo via \`gh api\`) and use it as the body structure, and search for matching open issues with \`gh issue list --search\` to include \`Closes #<n>\` / \`Refs #<n>\` links.
1665
1670
  - Do NOT create branches, commits, push changes, or open pull requests unless the user explicitly asks for that`;
1666
1671
 
1667
1672
  return `
@@ -1704,7 +1709,11 @@ After completing the requested changes:
1704
1709
  1. Create a new branch prefixed with \`posthog-code/\` (e.g. \`posthog-code/fix-login-redirect\`) based on the work done
1705
1710
  2. Stage and commit all changes with a clear commit message
1706
1711
  3. Push the branch to origin
1707
- 4. Create a draft pull request using \`gh pr create --draft${this.config.baseBranch ? ` --base ${this.config.baseBranch}` : ""}\` with a descriptive title and body. Add the following footer at the end of the PR description:
1712
+ 4. Before opening the PR, prepare the body:
1713
+ - Check the repo for a PR template at \`.github/pull_request_template.md\` (also try \`.github/PULL_REQUEST_TEMPLATE.md\`, \`docs/pull_request_template.md\`, and root variants). If one exists, use its exact section headings as the PR body — do NOT fall back to a generic Summary/Test plan format.
1714
+ - If no repo-level template exists, check the org's \`.github\` repo via \`gh api /repos/<owner>/.github/contents/.github/pull_request_template.md\` (and other common paths) and use that as a fallback.
1715
+ - Search for matching open issues with \`gh issue list --state open --search '<keywords>'\` (derive keywords from the branch name, commits, and changed files; \`gh issue view <n>\` to confirm relevance). For every issue this PR would resolve, include a \`Closes #<n>\` line in the body so GitHub auto-links and auto-closes it on merge. For issues that are related but not fully resolved, use \`Refs #<n>\` instead.
1716
+ 5. Create a draft pull request using \`gh pr create --draft${this.config.baseBranch ? ` --base ${this.config.baseBranch}` : ""}\` with a descriptive title and the body prepared above. Add the following footer at the end of the PR description:
1708
1717
  \`\`\`
1709
1718
  ---
1710
1719
  *Created with [PostHog Code](https://posthog.com/code?ref=pr)*