@posthog/agent 2.3.326 → 2.3.346
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/adapters/claude/conversion/tool-use-to-acp.d.ts +9 -0
- package/dist/adapters/claude/conversion/tool-use-to-acp.js +15 -1
- package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
- package/dist/adapters/claude/permissions/permission-options.js +18 -11
- package/dist/adapters/claude/permissions/permission-options.js.map +1 -1
- package/dist/adapters/claude/session/jsonl-hydration.js.map +1 -1
- package/dist/adapters/claude/session/models.d.ts +2 -2
- package/dist/adapters/claude/session/models.js +12 -6
- package/dist/adapters/claude/session/models.js.map +1 -1
- package/dist/adapters/claude/tools.js +15 -13
- package/dist/adapters/claude/tools.js.map +1 -1
- package/dist/adapters/reasoning-effort.d.ts +1 -1
- package/dist/adapters/reasoning-effort.js +11 -5
- package/dist/adapters/reasoning-effort.js.map +1 -1
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +6975 -613
- package/dist/agent.js.map +1 -1
- package/dist/execution-mode.d.ts +1 -1
- package/dist/execution-mode.js +14 -12
- package/dist/execution-mode.js.map +1 -1
- package/dist/posthog-api.d.ts +5 -3
- package/dist/posthog-api.js +12 -22
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.d.ts +8 -1
- package/dist/server/agent-server.js +11362 -4894
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +11423 -4954
- package/dist/server/bin.cjs.map +1 -1
- package/dist/types.d.ts +11 -1
- package/package.json +7 -6
- package/src/adapters/acp-connection.ts +14 -1
- package/src/adapters/claude/UPSTREAM.md +24 -4
- package/src/adapters/claude/claude-agent.ts +161 -14
- package/src/adapters/claude/conversion/acp-to-sdk.test.ts +49 -0
- package/src/adapters/claude/conversion/acp-to-sdk.ts +23 -6
- package/src/adapters/claude/conversion/sdk-to-acp.ts +14 -2
- package/src/adapters/claude/conversion/tool-use-to-acp.ts +18 -1
- package/src/adapters/claude/hooks.test.ts +189 -0
- package/src/adapters/claude/hooks.ts +93 -3
- package/src/adapters/claude/permissions/permission-handlers.ts +2 -1
- package/src/adapters/claude/permissions/permission-options.ts +5 -0
- package/src/adapters/claude/session/models.ts +11 -5
- package/src/adapters/claude/session/options.ts +19 -3
- package/src/adapters/claude/session/settings.ts +17 -9
- package/src/adapters/claude/tools.ts +1 -1
- package/src/adapters/claude/types.ts +8 -1
- package/src/adapters/codex/codex-agent.ts +15 -2
- package/src/adapters/codex/codex-client.test.ts +112 -0
- package/src/adapters/codex/codex-client.ts +14 -1
- package/src/adapters/reasoning-effort.ts +6 -1
- package/src/agent.ts +6 -0
- package/src/enrichment/file-enricher.test.ts +163 -0
- package/src/enrichment/file-enricher.ts +82 -0
- package/src/execution-mode.test.ts +1 -0
- package/src/execution-mode.ts +13 -11
- package/src/posthog-api.test.ts +32 -0
- package/src/posthog-api.ts +13 -30
- package/src/server/agent-server.test.ts +96 -0
- package/src/server/agent-server.ts +207 -11
- package/src/server/bin.ts +1 -1
- package/src/server/schemas.test.ts +10 -0
- package/src/server/schemas.ts +25 -6
- package/src/server/types.ts +1 -1
- package/src/test/mocks/msw-handlers.ts +4 -1
- package/src/types.ts +10 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentSideConnection,
|
|
3
|
+
ReadTextFileRequest,
|
|
4
|
+
ReadTextFileResponse,
|
|
5
|
+
} from "@agentclientprotocol/sdk";
|
|
6
|
+
import { describe, expect, test, vi } from "vitest";
|
|
7
|
+
import type { FileEnrichmentDeps } from "../../enrichment/file-enricher";
|
|
8
|
+
import { Logger } from "../../utils/logger";
|
|
9
|
+
|
|
10
|
+
const enrichFileMock = vi.hoisted(() => vi.fn());
|
|
11
|
+
vi.mock("../../enrichment/file-enricher", () => ({
|
|
12
|
+
enrichFileForAgent: enrichFileMock,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import { createCodexClient } from "./codex-client";
|
|
16
|
+
import { createSessionState } from "./session-state";
|
|
17
|
+
|
|
18
|
+
function makeUpstream(response: ReadTextFileResponse): AgentSideConnection & {
|
|
19
|
+
readTextFile: ReturnType<typeof vi.fn>;
|
|
20
|
+
} {
|
|
21
|
+
const mock = {
|
|
22
|
+
readTextFile: vi.fn(async (_: ReadTextFileRequest) => response),
|
|
23
|
+
writeTextFile: vi.fn(),
|
|
24
|
+
requestPermission: vi.fn(),
|
|
25
|
+
sessionUpdate: vi.fn(),
|
|
26
|
+
createTerminal: vi.fn(),
|
|
27
|
+
terminalOutput: vi.fn(),
|
|
28
|
+
releaseTerminal: vi.fn(),
|
|
29
|
+
waitForTerminalExit: vi.fn(),
|
|
30
|
+
killTerminal: vi.fn(),
|
|
31
|
+
extMethod: vi.fn(),
|
|
32
|
+
extNotification: vi.fn(),
|
|
33
|
+
};
|
|
34
|
+
return mock as unknown as AgentSideConnection & {
|
|
35
|
+
readTextFile: ReturnType<typeof vi.fn>;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("createCodexClient readTextFile", () => {
|
|
40
|
+
const logger = new Logger({ debug: false, prefix: "[test]" });
|
|
41
|
+
const sessionState = createSessionState("", "/tmp");
|
|
42
|
+
|
|
43
|
+
test("returns upstream response unchanged when enrichmentDeps is absent", async () => {
|
|
44
|
+
enrichFileMock.mockReset();
|
|
45
|
+
const upstream = makeUpstream({ content: "const x = 1;" });
|
|
46
|
+
const client = createCodexClient(upstream, logger, sessionState);
|
|
47
|
+
|
|
48
|
+
const result = await client.readTextFile?.({
|
|
49
|
+
sessionId: "s",
|
|
50
|
+
path: "/tmp/a.ts",
|
|
51
|
+
});
|
|
52
|
+
expect(result?.content).toBe("const x = 1;");
|
|
53
|
+
expect(enrichFileMock).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("returns enriched content when helper returns a string", async () => {
|
|
57
|
+
enrichFileMock.mockReset();
|
|
58
|
+
enrichFileMock.mockResolvedValueOnce("const x = 1; // [PostHog] Flag ...");
|
|
59
|
+
|
|
60
|
+
const upstream = makeUpstream({ content: "const x = 1;" });
|
|
61
|
+
const deps = {} as FileEnrichmentDeps;
|
|
62
|
+
const client = createCodexClient(upstream, logger, sessionState, {
|
|
63
|
+
enrichmentDeps: deps,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const result = await client.readTextFile?.({
|
|
67
|
+
sessionId: "s",
|
|
68
|
+
path: "/tmp/a.ts",
|
|
69
|
+
});
|
|
70
|
+
expect(result?.content).toBe("const x = 1; // [PostHog] Flag ...");
|
|
71
|
+
expect(enrichFileMock).toHaveBeenCalledWith(
|
|
72
|
+
deps,
|
|
73
|
+
"/tmp/a.ts",
|
|
74
|
+
"const x = 1;",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("falls back to upstream response when helper returns null", async () => {
|
|
79
|
+
enrichFileMock.mockReset();
|
|
80
|
+
enrichFileMock.mockResolvedValueOnce(null);
|
|
81
|
+
|
|
82
|
+
const upstream = makeUpstream({ content: "no posthog here" });
|
|
83
|
+
const client = createCodexClient(upstream, logger, sessionState, {
|
|
84
|
+
enrichmentDeps: {} as FileEnrichmentDeps,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = await client.readTextFile?.({
|
|
88
|
+
sessionId: "s",
|
|
89
|
+
path: "/tmp/a.ts",
|
|
90
|
+
});
|
|
91
|
+
expect(result?.content).toBe("no posthog here");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("calls upstream.readTextFile with original params (UI sees original)", async () => {
|
|
95
|
+
enrichFileMock.mockReset();
|
|
96
|
+
enrichFileMock.mockResolvedValueOnce("enriched");
|
|
97
|
+
|
|
98
|
+
const upstream = makeUpstream({ content: "original" });
|
|
99
|
+
const client = createCodexClient(upstream, logger, sessionState, {
|
|
100
|
+
enrichmentDeps: {} as FileEnrichmentDeps,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const params = {
|
|
104
|
+
sessionId: "s",
|
|
105
|
+
path: "/tmp/a.ts",
|
|
106
|
+
line: 10,
|
|
107
|
+
limit: 5,
|
|
108
|
+
};
|
|
109
|
+
await client.readTextFile?.(params);
|
|
110
|
+
expect(upstream.readTextFile).toHaveBeenCalledWith(params);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -29,6 +29,10 @@ import type {
|
|
|
29
29
|
WriteTextFileRequest,
|
|
30
30
|
WriteTextFileResponse,
|
|
31
31
|
} from "@agentclientprotocol/sdk";
|
|
32
|
+
import {
|
|
33
|
+
enrichFileForAgent,
|
|
34
|
+
type FileEnrichmentDeps,
|
|
35
|
+
} from "../../enrichment/file-enricher";
|
|
32
36
|
import type { PermissionMode } from "../../execution-mode";
|
|
33
37
|
import type { Logger } from "../../utils/logger";
|
|
34
38
|
import type { CodexSessionState } from "./session-state";
|
|
@@ -36,6 +40,8 @@ import type { CodexSessionState } from "./session-state";
|
|
|
36
40
|
export interface CodexClientCallbacks {
|
|
37
41
|
/** Called when a usage_update session notification is received */
|
|
38
42
|
onUsageUpdate?: (update: Record<string, unknown>) => void;
|
|
43
|
+
/** When set, Read responses are annotated with PostHog enrichment before reaching codex-acp. */
|
|
44
|
+
enrichmentDeps?: FileEnrichmentDeps;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
const AUTO_APPROVED_KINDS: Record<PermissionMode, Set<ToolKind>> = {
|
|
@@ -152,7 +158,14 @@ export function createCodexClient(
|
|
|
152
158
|
async readTextFile(
|
|
153
159
|
params: ReadTextFileRequest,
|
|
154
160
|
): Promise<ReadTextFileResponse> {
|
|
155
|
-
|
|
161
|
+
const response = await upstreamClient.readTextFile(params);
|
|
162
|
+
if (!callbacks?.enrichmentDeps) return response;
|
|
163
|
+
const enriched = await enrichFileForAgent(
|
|
164
|
+
callbacks.enrichmentDeps,
|
|
165
|
+
params.path,
|
|
166
|
+
response.content,
|
|
167
|
+
);
|
|
168
|
+
return enriched ? { ...response, content: enriched } : response;
|
|
156
169
|
},
|
|
157
170
|
|
|
158
171
|
async writeTextFile(
|
|
@@ -3,7 +3,12 @@ import { getReasoningEffortOptions as getCodexReasoningEffortOptions } from "./c
|
|
|
3
3
|
|
|
4
4
|
export type RuntimeAdapter = "claude" | "codex";
|
|
5
5
|
|
|
6
|
-
export type SupportedReasoningEffort =
|
|
6
|
+
export type SupportedReasoningEffort =
|
|
7
|
+
| "low"
|
|
8
|
+
| "medium"
|
|
9
|
+
| "high"
|
|
10
|
+
| "xhigh"
|
|
11
|
+
| "max";
|
|
7
12
|
|
|
8
13
|
export interface ReasoningEffortOption {
|
|
9
14
|
value: SupportedReasoningEffort;
|
package/src/agent.ts
CHANGED
|
@@ -19,6 +19,8 @@ export class Agent {
|
|
|
19
19
|
private acpConnection?: InProcessAcpConnection;
|
|
20
20
|
private taskRunId?: string;
|
|
21
21
|
private sessionLogWriter?: SessionLogWriter;
|
|
22
|
+
private posthogApiConfig?: AgentConfig["posthog"];
|
|
23
|
+
private enricherEnabled: boolean;
|
|
22
24
|
|
|
23
25
|
constructor(config: AgentConfig) {
|
|
24
26
|
this.logger = new Logger({
|
|
@@ -29,7 +31,9 @@ export class Agent {
|
|
|
29
31
|
|
|
30
32
|
if (config.posthog) {
|
|
31
33
|
this.posthogAPI = new PostHogAPIClient(config.posthog);
|
|
34
|
+
this.posthogApiConfig = config.posthog;
|
|
32
35
|
}
|
|
36
|
+
this.enricherEnabled = config.enricher?.enabled !== false;
|
|
33
37
|
|
|
34
38
|
if (config.posthog && !config.skipLogPersistence) {
|
|
35
39
|
this.sessionLogWriter = new SessionLogWriter({
|
|
@@ -121,6 +125,8 @@ export class Agent {
|
|
|
121
125
|
processCallbacks: options.processCallbacks,
|
|
122
126
|
onStructuredOutput: options.onStructuredOutput,
|
|
123
127
|
allowedModelIds,
|
|
128
|
+
posthogApiConfig: this.posthogApiConfig,
|
|
129
|
+
enricherEnabled: this.enricherEnabled,
|
|
124
130
|
codexOptions:
|
|
125
131
|
options.adapter === "codex" && gatewayConfig
|
|
126
132
|
? {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { enrichFileForAgent, type FileEnrichmentDeps } from "./file-enricher";
|
|
3
|
+
|
|
4
|
+
function makeDeps(overrides: {
|
|
5
|
+
toInlineCommentsReturn?: string;
|
|
6
|
+
callsCount?: number;
|
|
7
|
+
initCallsCount?: number;
|
|
8
|
+
parseRejects?: Error;
|
|
9
|
+
isSupported?: boolean;
|
|
10
|
+
getApiKey?: () => string | Promise<string>;
|
|
11
|
+
}): {
|
|
12
|
+
deps: FileEnrichmentDeps;
|
|
13
|
+
parseSpy: ReturnType<typeof vi.fn>;
|
|
14
|
+
enrichFromApiSpy: ReturnType<typeof vi.fn>;
|
|
15
|
+
getApiKeySpy: ReturnType<typeof vi.fn>;
|
|
16
|
+
} {
|
|
17
|
+
const enrichFromApiSpy = vi.fn(async () => ({
|
|
18
|
+
toInlineComments: () =>
|
|
19
|
+
overrides.toInlineCommentsReturn ?? "enriched content",
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const parseSpy = vi.fn(async () => {
|
|
23
|
+
if (overrides.parseRejects) throw overrides.parseRejects;
|
|
24
|
+
return {
|
|
25
|
+
calls: Array.from({ length: overrides.callsCount ?? 1 }),
|
|
26
|
+
initCalls: Array.from({ length: overrides.initCallsCount ?? 0 }),
|
|
27
|
+
enrichFromApi: enrichFromApiSpy,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const getApiKeySpy = vi.fn(overrides.getApiKey ?? (() => "phx_test"));
|
|
32
|
+
|
|
33
|
+
const deps: FileEnrichmentDeps = {
|
|
34
|
+
enricher: {
|
|
35
|
+
isSupported: vi.fn(() => overrides.isSupported ?? true),
|
|
36
|
+
parse: parseSpy,
|
|
37
|
+
} as unknown as FileEnrichmentDeps["enricher"],
|
|
38
|
+
apiConfig: {
|
|
39
|
+
apiUrl: "https://test.posthog.com",
|
|
40
|
+
projectId: 1,
|
|
41
|
+
getApiKey: getApiKeySpy,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return { deps, parseSpy, enrichFromApiSpy, getApiKeySpy };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("enrichFileForAgent", () => {
|
|
49
|
+
test("returns null for unsupported extension", async () => {
|
|
50
|
+
const { deps, parseSpy } = makeDeps({});
|
|
51
|
+
const result = await enrichFileForAgent(
|
|
52
|
+
deps,
|
|
53
|
+
"/tmp/notes.txt",
|
|
54
|
+
"some text",
|
|
55
|
+
);
|
|
56
|
+
expect(result).toBeNull();
|
|
57
|
+
expect(parseSpy).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("returns null for empty content", async () => {
|
|
61
|
+
const { deps, parseSpy } = makeDeps({});
|
|
62
|
+
const result = await enrichFileForAgent(deps, "/tmp/code.ts", "");
|
|
63
|
+
expect(result).toBeNull();
|
|
64
|
+
expect(parseSpy).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("returns null for content > 1MB", async () => {
|
|
68
|
+
const { deps, parseSpy } = makeDeps({});
|
|
69
|
+
const huge = "x".repeat(1_000_001);
|
|
70
|
+
const result = await enrichFileForAgent(deps, "/tmp/code.ts", huge);
|
|
71
|
+
expect(result).toBeNull();
|
|
72
|
+
expect(parseSpy).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("returns null when language not supported by enricher", async () => {
|
|
76
|
+
const { deps, parseSpy } = makeDeps({ isSupported: false });
|
|
77
|
+
const result = await enrichFileForAgent(
|
|
78
|
+
deps,
|
|
79
|
+
"/tmp/code.ts",
|
|
80
|
+
"posthog.capture('x');",
|
|
81
|
+
);
|
|
82
|
+
expect(result).toBeNull();
|
|
83
|
+
expect(parseSpy).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("returns null when no PostHog calls detected", async () => {
|
|
87
|
+
const { deps, enrichFromApiSpy } = makeDeps({
|
|
88
|
+
callsCount: 0,
|
|
89
|
+
initCallsCount: 0,
|
|
90
|
+
});
|
|
91
|
+
const result = await enrichFileForAgent(
|
|
92
|
+
deps,
|
|
93
|
+
"/tmp/code.ts",
|
|
94
|
+
"posthog.capture('x');",
|
|
95
|
+
);
|
|
96
|
+
expect(result).toBeNull();
|
|
97
|
+
expect(enrichFromApiSpy).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("returns null and skips parse when content has no posthog reference", async () => {
|
|
101
|
+
const { deps, parseSpy } = makeDeps({});
|
|
102
|
+
const result = await enrichFileForAgent(
|
|
103
|
+
deps,
|
|
104
|
+
"/tmp/code.ts",
|
|
105
|
+
"const x = 1;\nfunction foo() {}",
|
|
106
|
+
);
|
|
107
|
+
expect(result).toBeNull();
|
|
108
|
+
expect(parseSpy).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("returns null when getApiKey yields empty string", async () => {
|
|
112
|
+
const { deps, enrichFromApiSpy } = makeDeps({ getApiKey: () => "" });
|
|
113
|
+
const result = await enrichFileForAgent(
|
|
114
|
+
deps,
|
|
115
|
+
"/tmp/code.ts",
|
|
116
|
+
"posthog.capture('x');",
|
|
117
|
+
);
|
|
118
|
+
expect(result).toBeNull();
|
|
119
|
+
expect(enrichFromApiSpy).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("returns null when toInlineComments produces no change", async () => {
|
|
123
|
+
const original = "posthog.capture('x');";
|
|
124
|
+
const { deps } = makeDeps({ toInlineCommentsReturn: original });
|
|
125
|
+
const result = await enrichFileForAgent(deps, "/tmp/code.ts", original);
|
|
126
|
+
expect(result).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("returns null and logs debug when enricher throws", async () => {
|
|
130
|
+
const logger = { debug: vi.fn() };
|
|
131
|
+
const { deps } = makeDeps({ parseRejects: new Error("boom") });
|
|
132
|
+
deps.logger = logger as unknown as FileEnrichmentDeps["logger"];
|
|
133
|
+
const result = await enrichFileForAgent(
|
|
134
|
+
deps,
|
|
135
|
+
"/tmp/code.ts",
|
|
136
|
+
"posthog.capture('x');",
|
|
137
|
+
);
|
|
138
|
+
expect(result).toBeNull();
|
|
139
|
+
expect(logger.debug).toHaveBeenCalledWith(
|
|
140
|
+
"File enrichment failed",
|
|
141
|
+
expect.objectContaining({ filePath: "/tmp/code.ts" }),
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("returns enriched string when happy path completes", async () => {
|
|
146
|
+
const { deps, enrichFromApiSpy } = makeDeps({
|
|
147
|
+
toInlineCommentsReturn: "posthog.capture('x'); // [PostHog] Event: \"x\"",
|
|
148
|
+
});
|
|
149
|
+
const result = await enrichFileForAgent(
|
|
150
|
+
deps,
|
|
151
|
+
"/tmp/code.ts",
|
|
152
|
+
"posthog.capture('x');",
|
|
153
|
+
);
|
|
154
|
+
expect(result).toBe("posthog.capture('x'); // [PostHog] Event: \"x\"");
|
|
155
|
+
expect(enrichFromApiSpy).toHaveBeenCalledWith(
|
|
156
|
+
expect.objectContaining({
|
|
157
|
+
apiKey: "phx_test",
|
|
158
|
+
host: "https://test.posthog.com",
|
|
159
|
+
projectId: 1,
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { EXT_TO_LANG_ID, PostHogEnricher } from "@posthog/enricher";
|
|
3
|
+
import type { PostHogAPIConfig } from "../types";
|
|
4
|
+
import type { Logger } from "../utils/logger";
|
|
5
|
+
|
|
6
|
+
export interface FileEnrichmentDeps {
|
|
7
|
+
enricher: PostHogEnricher;
|
|
8
|
+
apiConfig: PostHogAPIConfig;
|
|
9
|
+
logger?: Logger;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Enrichment {
|
|
13
|
+
deps: FileEnrichmentDeps;
|
|
14
|
+
dispose(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createEnrichment(
|
|
18
|
+
apiConfig: PostHogAPIConfig | undefined,
|
|
19
|
+
logger?: Logger,
|
|
20
|
+
): Enrichment | undefined {
|
|
21
|
+
if (!apiConfig) return undefined;
|
|
22
|
+
const enricher = new PostHogEnricher();
|
|
23
|
+
return {
|
|
24
|
+
deps: { enricher, apiConfig, logger },
|
|
25
|
+
dispose: () => enricher.dispose(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const MAX_ENRICHMENT_BYTES = 1_000_000;
|
|
30
|
+
|
|
31
|
+
export async function enrichFileForAgent(
|
|
32
|
+
deps: FileEnrichmentDeps,
|
|
33
|
+
filePath: string,
|
|
34
|
+
content: string,
|
|
35
|
+
): Promise<string | null> {
|
|
36
|
+
if (!content || content.length > MAX_ENRICHMENT_BYTES) return null;
|
|
37
|
+
|
|
38
|
+
// Skip the tree-sitter parse for files with no PostHog references.
|
|
39
|
+
if (!/posthog/i.test(content)) return null;
|
|
40
|
+
|
|
41
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
42
|
+
const langId = EXT_TO_LANG_ID[ext];
|
|
43
|
+
if (!langId || !deps.enricher.isSupported(langId)) return null;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const parsed = await deps.enricher.parse(content, langId);
|
|
47
|
+
if (parsed.calls.length === 0 && parsed.initCalls.length === 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const apiKey = await deps.apiConfig.getApiKey();
|
|
52
|
+
if (!apiKey) return null;
|
|
53
|
+
|
|
54
|
+
const enriched = await parsed.enrichFromApi({
|
|
55
|
+
apiKey,
|
|
56
|
+
host: deps.apiConfig.apiUrl,
|
|
57
|
+
projectId: deps.apiConfig.projectId,
|
|
58
|
+
timeoutMs: 5_000,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const annotated = enriched.toInlineComments();
|
|
62
|
+
if (annotated === content) {
|
|
63
|
+
deps.logger?.debug("File enrichment produced no changes", {
|
|
64
|
+
filePath,
|
|
65
|
+
calls: parsed.calls.length,
|
|
66
|
+
});
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
deps.logger?.debug("File enriched", {
|
|
70
|
+
filePath,
|
|
71
|
+
calls: parsed.calls.length,
|
|
72
|
+
});
|
|
73
|
+
return annotated;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const detail =
|
|
76
|
+
err instanceof Error
|
|
77
|
+
? { message: err.message, name: err.name, stack: err.stack }
|
|
78
|
+
: { value: String(err) };
|
|
79
|
+
deps.logger?.debug("File enrichment failed", { filePath, ...detail });
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/execution-mode.ts
CHANGED
|
@@ -25,19 +25,21 @@ const availableModes: ModeInfo[] = [
|
|
|
25
25
|
name: "Plan Mode",
|
|
26
26
|
description: "Planning mode, no actual tool execution",
|
|
27
27
|
},
|
|
28
|
-
// {
|
|
29
|
-
// id: "dontAsk",
|
|
30
|
-
// name: "Don't Ask",
|
|
31
|
-
// description: "Don't prompt for permissions, deny if not pre-approved",
|
|
32
|
-
// },
|
|
33
28
|
];
|
|
34
29
|
|
|
35
30
|
if (ALLOW_BYPASS) {
|
|
36
|
-
availableModes.push(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
availableModes.push(
|
|
32
|
+
{
|
|
33
|
+
id: "bypassPermissions",
|
|
34
|
+
name: "Bypass Permissions",
|
|
35
|
+
description: "Auto-accept all permission requests",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "auto",
|
|
39
|
+
name: "Auto Mode",
|
|
40
|
+
description: "Use a model classifier to approve/deny permission prompts",
|
|
41
|
+
},
|
|
42
|
+
);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
// Expose execution mode IDs in type-safe order for type checks
|
|
@@ -45,8 +47,8 @@ export const CODE_EXECUTION_MODES = [
|
|
|
45
47
|
"default",
|
|
46
48
|
"acceptEdits",
|
|
47
49
|
"plan",
|
|
48
|
-
// "dontAsk",
|
|
49
50
|
"bypassPermissions",
|
|
51
|
+
"auto",
|
|
50
52
|
] as const;
|
|
51
53
|
|
|
52
54
|
export type CodeExecutionMode = (typeof CODE_EXECUTION_MODES)[number];
|
package/src/posthog-api.test.ts
CHANGED
|
@@ -45,4 +45,36 @@ describe("PostHogAPIClient", () => {
|
|
|
45
45
|
expect(refreshApiKey).toHaveBeenCalledTimes(1);
|
|
46
46
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
it("downloads artifacts through the backend endpoint", async () => {
|
|
50
|
+
const client = new PostHogAPIClient({
|
|
51
|
+
apiUrl: "https://app.posthog.com",
|
|
52
|
+
getApiKey: vi.fn().mockResolvedValue("token"),
|
|
53
|
+
projectId: 7,
|
|
54
|
+
});
|
|
55
|
+
const bytes = new TextEncoder().encode("hello world");
|
|
56
|
+
|
|
57
|
+
mockFetch.mockResolvedValueOnce({
|
|
58
|
+
ok: true,
|
|
59
|
+
arrayBuffer: vi.fn().mockResolvedValue(bytes.buffer),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const artifact = await client.downloadArtifact(
|
|
63
|
+
"task-1",
|
|
64
|
+
"run-1",
|
|
65
|
+
"tasks/artifacts/team_1/task_task-1/run_run-1/file.txt",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(artifact).toEqual(bytes.buffer);
|
|
69
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
70
|
+
"https://app.posthog.com/api/projects/7/tasks/task-1/runs/run-1/artifacts/download/",
|
|
71
|
+
expect.objectContaining({
|
|
72
|
+
method: "POST",
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
storage_path: "tasks/artifacts/team_1/task_task-1/run_run-1/file.txt",
|
|
75
|
+
}),
|
|
76
|
+
headers: expect.any(Headers),
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
48
80
|
});
|
package/src/posthog-api.ts
CHANGED
|
@@ -31,7 +31,9 @@ export type TaskRunUpdate = Partial<
|
|
|
31
31
|
| "state"
|
|
32
32
|
| "environment"
|
|
33
33
|
>
|
|
34
|
-
|
|
34
|
+
> & {
|
|
35
|
+
state_remove_keys?: string[];
|
|
36
|
+
};
|
|
35
37
|
|
|
36
38
|
export class PostHogAPIClient {
|
|
37
39
|
private config: PostHogAPIConfig;
|
|
@@ -223,45 +225,26 @@ export class PostHogAPIClient {
|
|
|
223
225
|
return response.artifacts ?? [];
|
|
224
226
|
}
|
|
225
227
|
|
|
226
|
-
async getArtifactPresignedUrl(
|
|
227
|
-
taskId: string,
|
|
228
|
-
runId: string,
|
|
229
|
-
storagePath: string,
|
|
230
|
-
): Promise<string | null> {
|
|
231
|
-
const teamId = this.getTeamId();
|
|
232
|
-
try {
|
|
233
|
-
const response = await this.apiRequest<{
|
|
234
|
-
url: string;
|
|
235
|
-
expires_in: number;
|
|
236
|
-
}>(
|
|
237
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/presign/`,
|
|
238
|
-
{
|
|
239
|
-
method: "POST",
|
|
240
|
-
body: JSON.stringify({ storage_path: storagePath }),
|
|
241
|
-
},
|
|
242
|
-
);
|
|
243
|
-
return response.url;
|
|
244
|
-
} catch {
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
228
|
/**
|
|
250
229
|
* Download artifact content by storage path
|
|
251
|
-
*
|
|
230
|
+
* Streams the file through the PostHog backend so the sandbox does not need
|
|
231
|
+
* direct access to object storage.
|
|
252
232
|
*/
|
|
253
233
|
async downloadArtifact(
|
|
254
234
|
taskId: string,
|
|
255
235
|
runId: string,
|
|
256
236
|
storagePath: string,
|
|
257
237
|
): Promise<ArrayBuffer | null> {
|
|
258
|
-
const
|
|
259
|
-
if (!url) {
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
238
|
+
const teamId = this.getTeamId();
|
|
262
239
|
|
|
263
240
|
try {
|
|
264
|
-
const response = await
|
|
241
|
+
const response = await this.performRequestWithRetry(
|
|
242
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/download/`,
|
|
243
|
+
{
|
|
244
|
+
method: "POST",
|
|
245
|
+
body: JSON.stringify({ storage_path: storagePath }),
|
|
246
|
+
},
|
|
247
|
+
);
|
|
265
248
|
if (!response.ok) {
|
|
266
249
|
throw new Error(`Failed to download artifact: ${response.status}`);
|
|
267
250
|
}
|