@posthog/agent 2.3.366 → 2.3.385
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/dist/agent.js +1087 -204
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.js +1 -1
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.js +1016 -133
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +1004 -121
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/claude/claude-agent.ts +20 -17
- package/src/enrichment/file-enricher.test.ts +137 -3
- package/src/enrichment/file-enricher.ts +96 -5
package/package.json
CHANGED
|
@@ -316,15 +316,6 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
|
319
|
-
this.session.cancelled = false;
|
|
320
|
-
this.session.interruptReason = undefined;
|
|
321
|
-
this.session.accumulatedUsage = {
|
|
322
|
-
inputTokens: 0,
|
|
323
|
-
outputTokens: 0,
|
|
324
|
-
cachedReadTokens: 0,
|
|
325
|
-
cachedWriteTokens: 0,
|
|
326
|
-
};
|
|
327
|
-
|
|
328
319
|
const userMessage = promptToClaude(params);
|
|
329
320
|
const promptUuid = randomUUID();
|
|
330
321
|
userMessage.uuid = promptUuid;
|
|
@@ -364,7 +355,19 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
364
355
|
this.session.input.push(userMessage);
|
|
365
356
|
}
|
|
366
357
|
|
|
367
|
-
//
|
|
358
|
+
// Reset session state here (after the queued-wait) rather than at the
|
|
359
|
+
// top of prompt(). Otherwise a new prompt() call would wipe cancelled=true
|
|
360
|
+
// on the previous still-running loop, causing it to return end_turn
|
|
361
|
+
// instead of the cancelled stop reason the spec requires.
|
|
362
|
+
this.session.cancelled = false;
|
|
363
|
+
this.session.interruptReason = undefined;
|
|
364
|
+
this.session.accumulatedUsage = {
|
|
365
|
+
inputTokens: 0,
|
|
366
|
+
outputTokens: 0,
|
|
367
|
+
cachedReadTokens: 0,
|
|
368
|
+
cachedWriteTokens: 0,
|
|
369
|
+
};
|
|
370
|
+
|
|
368
371
|
await this.broadcastUserMessage(params);
|
|
369
372
|
|
|
370
373
|
this.session.promptRunning = true;
|
|
@@ -652,10 +655,6 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
652
655
|
|
|
653
656
|
case "user":
|
|
654
657
|
case "assistant": {
|
|
655
|
-
if (this.session.cancelled) {
|
|
656
|
-
break;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
658
|
// Check for prompt replay (our own message echoed back)
|
|
660
659
|
if (message.type === "user" && "uuid" in message && message.uuid) {
|
|
661
660
|
if (message.uuid === promptUuid) {
|
|
@@ -670,12 +669,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
670
669
|
pending.resolve(false);
|
|
671
670
|
this.session.pendingMessages.delete(message.uuid as string);
|
|
672
671
|
handedOff = true;
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
672
|
+
return {
|
|
673
|
+
stopReason: this.session.cancelled ? "cancelled" : "end_turn",
|
|
674
|
+
};
|
|
676
675
|
}
|
|
677
676
|
}
|
|
678
677
|
|
|
678
|
+
if (this.session.cancelled) {
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
|
|
679
682
|
// Skip replayed user messages that aren't pending prompts
|
|
680
683
|
if (
|
|
681
684
|
"isReplay" in message &&
|
|
@@ -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 {
|
|
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 {
|
|
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
|
+
}
|