@posthog/agent 2.3.363 → 2.3.370

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.363",
3
+ "version": "2.3.370",
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": {
@@ -87,8 +87,8 @@
87
87
  "typescript": "^5.5.0",
88
88
  "vitest": "^2.1.8",
89
89
  "@posthog/shared": "1.0.0",
90
- "@posthog/enricher": "1.0.0",
91
- "@posthog/git": "1.0.0"
90
+ "@posthog/git": "1.0.0",
91
+ "@posthog/enricher": "1.0.0"
92
92
  },
93
93
  "dependencies": {
94
94
  "@agentclientprotocol/sdk": "0.19.0",
@@ -8,11 +8,15 @@ function makeDeps(overrides: {
8
8
  parseRejects?: Error;
9
9
  isSupported?: boolean;
10
10
  getApiKey?: () => string | Promise<string>;
11
+ findImportsInSource?: () => Promise<unknown[]>;
12
+ getWrappersForFile?: () => Promise<unknown[]>;
11
13
  }): {
12
14
  deps: FileEnrichmentDeps;
13
15
  parseSpy: ReturnType<typeof vi.fn>;
14
16
  enrichFromApiSpy: ReturnType<typeof vi.fn>;
15
17
  getApiKeySpy: ReturnType<typeof vi.fn>;
18
+ findImportsSpy: ReturnType<typeof vi.fn>;
19
+ getWrappersSpy: ReturnType<typeof vi.fn>;
16
20
  } {
17
21
  const enrichFromApiSpy = vi.fn(async () => ({
18
22
  toInlineComments: () =>
@@ -29,11 +33,19 @@ function makeDeps(overrides: {
29
33
  });
30
34
 
31
35
  const getApiKeySpy = vi.fn(overrides.getApiKey ?? (() => "phx_test"));
36
+ const findImportsSpy = vi.fn(
37
+ overrides.findImportsInSource ?? (async () => []),
38
+ );
39
+ const getWrappersSpy = vi.fn(
40
+ overrides.getWrappersForFile ?? (async () => []),
41
+ );
32
42
 
33
43
  const deps: FileEnrichmentDeps = {
34
44
  enricher: {
35
45
  isSupported: vi.fn(() => overrides.isSupported ?? true),
36
46
  parse: parseSpy,
47
+ findImportsInSource: findImportsSpy,
48
+ getWrappersForFile: getWrappersSpy,
37
49
  } as unknown as FileEnrichmentDeps["enricher"],
38
50
  apiConfig: {
39
51
  apiUrl: "https://test.posthog.com",
@@ -42,7 +54,14 @@ function makeDeps(overrides: {
42
54
  },
43
55
  };
44
56
 
45
- return { deps, parseSpy, enrichFromApiSpy, getApiKeySpy };
57
+ return {
58
+ deps,
59
+ parseSpy,
60
+ enrichFromApiSpy,
61
+ getApiKeySpy,
62
+ findImportsSpy,
63
+ getWrappersSpy,
64
+ };
46
65
  }
47
66
 
48
67
  describe("enrichFileForAgent", () => {
@@ -97,8 +116,8 @@ describe("enrichFileForAgent", () => {
97
116
  expect(enrichFromApiSpy).not.toHaveBeenCalled();
98
117
  });
99
118
 
100
- test("returns null and skips parse when content has no posthog reference", async () => {
101
- const { deps, parseSpy } = makeDeps({});
119
+ test("returns null and skips parse when content has no posthog reference AND no relative imports", async () => {
120
+ const { deps, parseSpy, findImportsSpy } = makeDeps({});
102
121
  const result = await enrichFileForAgent(
103
122
  deps,
104
123
  "/tmp/code.ts",
@@ -106,6 +125,121 @@ describe("enrichFileForAgent", () => {
106
125
  );
107
126
  expect(result).toBeNull();
108
127
  expect(parseSpy).not.toHaveBeenCalled();
128
+ expect(findImportsSpy).not.toHaveBeenCalled();
129
+ });
130
+
131
+ test("relative import with no resolvable wrapper → skips parse", async () => {
132
+ const { deps, parseSpy, findImportsSpy, getWrappersSpy } = makeDeps({
133
+ findImportsInSource: async () => [
134
+ {
135
+ localName: "foo",
136
+ importedName: "foo",
137
+ resolvedAbsPath: "/tmp/foo.ts",
138
+ },
139
+ ],
140
+ getWrappersForFile: async () => [],
141
+ });
142
+ const result = await enrichFileForAgent(
143
+ deps,
144
+ "/tmp/app.ts",
145
+ 'import { foo } from "./foo";\nfoo("x");',
146
+ );
147
+ expect(result).toBeNull();
148
+ expect(findImportsSpy).toHaveBeenCalled();
149
+ expect(getWrappersSpy).toHaveBeenCalledWith("/tmp/foo.ts");
150
+ expect(parseSpy).not.toHaveBeenCalled();
151
+ });
152
+
153
+ test("relative import hitting a named wrapper triggers parse with context", async () => {
154
+ const wrapper = {
155
+ name: "track",
156
+ methodKind: "capture",
157
+ posthogMethod: "capture",
158
+ classification: { kind: "pass-through", paramIndex: 0 },
159
+ isNamedExport: true,
160
+ isDefaultExport: false,
161
+ };
162
+ const { deps, parseSpy, findImportsSpy, getWrappersSpy } = makeDeps({
163
+ findImportsInSource: async () => [
164
+ {
165
+ localName: "track",
166
+ importedName: "track",
167
+ resolvedAbsPath: "/tmp/telemetry.ts",
168
+ },
169
+ ],
170
+ getWrappersForFile: async () => [wrapper],
171
+ });
172
+ const result = await enrichFileForAgent(
173
+ deps,
174
+ "/tmp/app.ts",
175
+ 'import { track } from "./telemetry";\ntrack("x");',
176
+ );
177
+ expect(result).toBe("enriched content");
178
+ expect(findImportsSpy).toHaveBeenCalled();
179
+ expect(getWrappersSpy).toHaveBeenCalledWith("/tmp/telemetry.ts");
180
+ expect(parseSpy).toHaveBeenCalledWith(
181
+ expect.any(String),
182
+ expect.any(String),
183
+ expect.objectContaining({
184
+ wrappersByLocalName: expect.any(Map),
185
+ }),
186
+ );
187
+ const ctxArg = parseSpy.mock.calls[0][2] as {
188
+ wrappersByLocalName: Map<string, unknown>;
189
+ };
190
+ expect(ctxArg.wrappersByLocalName.get("track")).toEqual(wrapper);
191
+ });
192
+
193
+ test("file with posthog literal and no relative imports skips import resolution", async () => {
194
+ const { deps, findImportsSpy, parseSpy } = makeDeps({});
195
+ await enrichFileForAgent(deps, "/tmp/code.ts", "posthog.capture('x');");
196
+ expect(findImportsSpy).not.toHaveBeenCalled();
197
+ expect(parseSpy).toHaveBeenCalled();
198
+ });
199
+
200
+ test("file with only bare-package imports does not trigger import resolution", async () => {
201
+ const { deps, findImportsSpy } = makeDeps({});
202
+ const content = [
203
+ 'import React from "react";',
204
+ 'import { useState } from "react";',
205
+ 'import posthog from "posthog-js";',
206
+ "posthog.capture('x');",
207
+ ].join("\n");
208
+ await enrichFileForAgent(deps, "/tmp/page.tsx", content);
209
+ expect(findImportsSpy).not.toHaveBeenCalled();
210
+ });
211
+
212
+ test("file with direct posthog AND wrapper imports gets both enriched", async () => {
213
+ const wrapper = {
214
+ name: "track",
215
+ methodKind: "capture",
216
+ posthogMethod: "capture",
217
+ classification: { kind: "pass-through", paramIndex: 0 },
218
+ isNamedExport: true,
219
+ isDefaultExport: false,
220
+ };
221
+ const { deps, findImportsSpy, parseSpy } = makeDeps({
222
+ findImportsInSource: async () => [
223
+ {
224
+ localName: "track",
225
+ importedName: "track",
226
+ resolvedAbsPath: "/tmp/utils.ts",
227
+ },
228
+ ],
229
+ getWrappersForFile: async () => [wrapper],
230
+ });
231
+ const content = [
232
+ 'import posthog from "posthog-js";',
233
+ 'import { track } from "./utils";',
234
+ "posthog.capture('direct');",
235
+ 'track("wrapper_call");',
236
+ ].join("\n");
237
+ await enrichFileForAgent(deps, "/tmp/page.tsx", content);
238
+ expect(findImportsSpy).toHaveBeenCalled();
239
+ const ctx = parseSpy.mock.calls[0][2] as {
240
+ wrappersByLocalName: Map<string, unknown>;
241
+ };
242
+ expect(ctx.wrappersByLocalName.get("track")).toEqual(wrapper);
109
243
  });
110
244
 
111
245
  test("returns null when getApiKey yields empty string", async () => {
@@ -1,5 +1,11 @@
1
1
  import * as path from "node:path";
2
- import { EXT_TO_LANG_ID, PostHogEnricher } from "@posthog/enricher";
2
+ import {
3
+ EXT_TO_LANG_ID,
4
+ type ImportEdge,
5
+ type LocalWrapper,
6
+ type ParseContext,
7
+ PostHogEnricher,
8
+ } from "@posthog/enricher";
3
9
  import type { PostHogAPIConfig } from "../types";
4
10
  import type { Logger } from "../utils/logger";
5
11
 
@@ -27,6 +33,10 @@ export function createEnrichment(
27
33
  }
28
34
 
29
35
  const MAX_ENRICHMENT_BYTES = 1_000_000;
36
+ const MAX_RELATIVE_IMPORTS = 64;
37
+ const RELATIVE_IMPORT_REGEX =
38
+ /(?:^|\n)\s*(?:import\b[^\n]*['"]\.{1,2}\/|from\s+\.)/;
39
+ const POSTHOG_LITERAL_REGEX = /posthog/i;
30
40
 
31
41
  export async function enrichFileForAgent(
32
42
  deps: FileEnrichmentDeps,
@@ -35,15 +45,29 @@ export async function enrichFileForAgent(
35
45
  ): Promise<string | null> {
36
46
  if (!content || content.length > MAX_ENRICHMENT_BYTES) return null;
37
47
 
38
- // Skip the tree-sitter parse for files with no PostHog references.
39
- if (!/posthog/i.test(content)) return null;
40
-
41
48
  const ext = path.extname(filePath).toLowerCase();
42
49
  const langId = EXT_TO_LANG_ID[ext];
43
50
  if (!langId || !deps.enricher.isSupported(langId)) return null;
44
51
 
52
+ const hasPostHogLiteral = POSTHOG_LITERAL_REGEX.test(content);
53
+ const hasRelativeImport = RELATIVE_IMPORT_REGEX.test(content);
54
+ let parseContext: ParseContext | undefined;
55
+
56
+ // Build wrapper context whenever the file has relative imports — direct PostHog
57
+ // usage and wrapper usage can coexist in the same file, so we don't skip this
58
+ // just because `posthog` already appears literally.
59
+ if (hasRelativeImport) {
60
+ const absPath = path.resolve(filePath);
61
+ const ctx = await buildWrapperContext(deps, content, langId, absPath);
62
+ if (ctx) parseContext = ctx;
63
+ }
64
+
65
+ // Bail only when nothing at all could be enriched: no direct posthog literal
66
+ // AND no resolvable wrappers.
67
+ if (!hasPostHogLiteral && !parseContext) return null;
68
+
45
69
  try {
46
- const parsed = await deps.enricher.parse(content, langId);
70
+ const parsed = await deps.enricher.parse(content, langId, parseContext);
47
71
  if (parsed.calls.length === 0 && parsed.initCalls.length === 0) {
48
72
  return null;
49
73
  }
@@ -69,6 +93,7 @@ export async function enrichFileForAgent(
69
93
  deps.logger?.debug("File enriched", {
70
94
  filePath,
71
95
  calls: parsed.calls.length,
96
+ viaWrappers: parsed.calls.filter((c) => c.viaWrapper).length,
72
97
  });
73
98
  return annotated;
74
99
  } catch (err) {
@@ -80,3 +105,69 @@ export async function enrichFileForAgent(
80
105
  return null;
81
106
  }
82
107
  }
108
+
109
+ async function buildWrapperContext(
110
+ deps: FileEnrichmentDeps,
111
+ content: string,
112
+ langId: string,
113
+ absPath: string,
114
+ ): Promise<ParseContext | null> {
115
+ let edges: ImportEdge[];
116
+ try {
117
+ edges = await deps.enricher.findImportsInSource(content, langId, absPath);
118
+ } catch (err) {
119
+ deps.logger?.debug("Import resolution failed", {
120
+ absPath,
121
+ err: err instanceof Error ? err.message : String(err),
122
+ });
123
+ return null;
124
+ }
125
+
126
+ if (!edges.length) return null;
127
+ const bounded = edges.slice(0, MAX_RELATIVE_IMPORTS);
128
+
129
+ const wrappersByLocalName = new Map<string, LocalWrapper>();
130
+ const namespaceWrappers = new Map<string, Map<string, LocalWrapper>>();
131
+
132
+ const resolutions = await Promise.all(
133
+ bounded.map(async (edge) => {
134
+ if (!edge.resolvedAbsPath) return null;
135
+ const wrappers = await deps.enricher.getWrappersForFile(
136
+ edge.resolvedAbsPath,
137
+ );
138
+ if (!wrappers.length) return null;
139
+ return { edge, wrappers };
140
+ }),
141
+ );
142
+
143
+ for (const entry of resolutions) {
144
+ if (!entry) continue;
145
+ const { edge, wrappers } = entry;
146
+
147
+ if (edge.isNamespace) {
148
+ const nsMap = new Map<string, LocalWrapper>();
149
+ for (const w of wrappers) {
150
+ if (w.isNamedExport || w.isDefaultExport) {
151
+ nsMap.set(w.name, w);
152
+ }
153
+ }
154
+ if (nsMap.size) namespaceWrappers.set(edge.localName, nsMap);
155
+ continue;
156
+ }
157
+
158
+ if (edge.isDefault) {
159
+ const target = wrappers.find((w) => w.isDefaultExport);
160
+ if (target) wrappersByLocalName.set(edge.localName, target);
161
+ continue;
162
+ }
163
+
164
+ const target = wrappers.find(
165
+ (w) => w.name === edge.importedName && w.isNamedExport,
166
+ );
167
+ if (target) wrappersByLocalName.set(edge.localName, target);
168
+ }
169
+
170
+ if (!wrappersByLocalName.size && !namespaceWrappers.size) return null;
171
+
172
+ return { wrappersByLocalName, namespaceWrappers };
173
+ }
@@ -7,9 +7,17 @@ import type {
7
7
  TaskRun,
8
8
  TaskRunArtifact,
9
9
  } from "./types";
10
- import { getGatewayUsageUrl, getLlmGatewayUrl } from "./utils/gateway";
11
-
12
- export { getGatewayUsageUrl, getLlmGatewayUrl };
10
+ import {
11
+ getGatewayInvalidatePlanCacheUrl,
12
+ getGatewayUsageUrl,
13
+ getLlmGatewayUrl,
14
+ } from "./utils/gateway";
15
+
16
+ export {
17
+ getGatewayInvalidatePlanCacheUrl,
18
+ getGatewayUsageUrl,
19
+ getLlmGatewayUrl,
20
+ };
13
21
 
14
22
  const DEFAULT_USER_AGENT = `posthog/agent.hog.dev; version: ${packageJson.version}`;
15
23
 
@@ -29,3 +29,10 @@ export function getGatewayUsageUrl(
29
29
  ): string {
30
30
  return `${getGatewayBaseUrl(posthogHost)}/v1/usage/${product}`;
31
31
  }
32
+
33
+ export function getGatewayInvalidatePlanCacheUrl(
34
+ posthogHost: string,
35
+ product: GatewayProduct = "posthog_code",
36
+ ): string {
37
+ return `${getGatewayBaseUrl(posthogHost)}/v1/usage/${product}/invalidate-plan-cache`;
38
+ }