@posthog/agent 2.0.0 → 2.0.2
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/LICENSE +1 -1
- package/README.md +221 -219
- package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
- package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
- package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
- package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
- package/dist/adapters/claude/permissions/permission-options.js +117 -0
- package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
- package/dist/adapters/claude/questions/utils.d.ts +132 -0
- package/dist/adapters/claude/questions/utils.js +63 -0
- package/dist/adapters/claude/questions/utils.js.map +1 -0
- package/dist/adapters/claude/tools.d.ts +18 -0
- package/dist/adapters/claude/tools.js +95 -0
- package/dist/adapters/claude/tools.js.map +1 -0
- package/dist/agent-DBQY1BfC.d.ts +123 -0
- package/dist/agent.d.ts +5 -0
- package/dist/agent.js +3656 -0
- package/dist/agent.js.map +1 -0
- package/dist/claude-cli/cli.js +3695 -2746
- package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
- package/dist/gateway-models.d.ts +24 -0
- package/dist/gateway-models.js +93 -0
- package/dist/gateway-models.js.map +1 -0
- package/dist/index.d.ts +170 -1157
- package/dist/index.js +9373 -5135
- package/dist/index.js.map +1 -1
- package/dist/logger-DDBiMOOD.d.ts +24 -0
- package/dist/posthog-api.d.ts +40 -0
- package/dist/posthog-api.js +175 -0
- package/dist/posthog-api.js.map +1 -0
- package/dist/server/agent-server.d.ts +41 -0
- package/dist/server/agent-server.js +10503 -0
- package/dist/server/agent-server.js.map +1 -0
- package/dist/server/bin.d.ts +1 -0
- package/dist/server/bin.js +10558 -0
- package/dist/server/bin.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +65 -13
- package/src/acp-extensions.ts +98 -16
- package/src/adapters/acp-connection.ts +494 -0
- package/src/adapters/base-acp-agent.ts +150 -0
- package/src/adapters/claude/claude-agent.ts +596 -0
- package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
- package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
- package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
- package/src/adapters/claude/hooks.ts +64 -0
- package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
- package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
- package/src/adapters/claude/permissions/permission-options.ts +103 -0
- package/src/adapters/claude/plan/utils.ts +56 -0
- package/src/adapters/claude/questions/utils.ts +92 -0
- package/src/adapters/claude/session/commands.ts +38 -0
- package/src/adapters/claude/session/mcp-config.ts +37 -0
- package/src/adapters/claude/session/models.ts +12 -0
- package/src/adapters/claude/session/options.ts +236 -0
- package/src/adapters/claude/tool-meta.ts +143 -0
- package/src/adapters/claude/tools.ts +53 -688
- package/src/adapters/claude/types.ts +61 -0
- package/src/adapters/codex/spawn.ts +130 -0
- package/src/agent.ts +96 -587
- package/src/execution-mode.ts +43 -0
- package/src/gateway-models.ts +135 -0
- package/src/index.ts +79 -0
- package/src/otel-log-writer.test.ts +105 -0
- package/src/otel-log-writer.ts +94 -0
- package/src/posthog-api.ts +75 -235
- package/src/resume.ts +115 -0
- package/src/sagas/apply-snapshot-saga.test.ts +690 -0
- package/src/sagas/apply-snapshot-saga.ts +88 -0
- package/src/sagas/capture-tree-saga.test.ts +892 -0
- package/src/sagas/capture-tree-saga.ts +141 -0
- package/src/sagas/resume-saga.test.ts +558 -0
- package/src/sagas/resume-saga.ts +332 -0
- package/src/sagas/test-fixtures.ts +250 -0
- package/src/server/agent-server.test.ts +220 -0
- package/src/server/agent-server.ts +748 -0
- package/src/server/bin.ts +88 -0
- package/src/server/jwt.ts +65 -0
- package/src/server/schemas.ts +47 -0
- package/src/server/types.ts +13 -0
- package/src/server/utils/retry.test.ts +122 -0
- package/src/server/utils/retry.ts +61 -0
- package/src/server/utils/sse-parser.test.ts +93 -0
- package/src/server/utils/sse-parser.ts +46 -0
- package/src/session-log-writer.test.ts +140 -0
- package/src/session-log-writer.ts +137 -0
- package/src/test/assertions.ts +114 -0
- package/src/test/controllers/sse-controller.ts +107 -0
- package/src/test/fixtures/api.ts +111 -0
- package/src/test/fixtures/config.ts +33 -0
- package/src/test/fixtures/notifications.ts +92 -0
- package/src/test/mocks/claude-sdk.ts +251 -0
- package/src/test/mocks/msw-handlers.ts +48 -0
- package/src/test/setup.ts +114 -0
- package/src/test/wait.ts +41 -0
- package/src/tree-tracker.ts +173 -0
- package/src/types.ts +54 -137
- package/src/utils/acp-content.ts +58 -0
- package/src/utils/async-mutex.test.ts +104 -0
- package/src/utils/async-mutex.ts +31 -0
- package/src/utils/common.ts +15 -0
- package/src/utils/gateway.ts +9 -6
- package/src/utils/logger.ts +0 -30
- package/src/utils/streams.ts +220 -0
- package/CLAUDE.md +0 -331
- package/src/adapters/claude/claude.ts +0 -1947
- package/src/adapters/claude/mcp-server.ts +0 -810
- package/src/adapters/claude/utils.ts +0 -267
- package/src/adapters/connection.ts +0 -95
- package/src/file-manager.ts +0 -273
- package/src/git-manager.ts +0 -577
- package/src/schemas.ts +0 -241
- package/src/session-store.ts +0 -259
- package/src/task-manager.ts +0 -163
- package/src/todo-manager.ts +0 -180
- package/src/tools/registry.ts +0 -134
- package/src/tools/types.ts +0 -133
- package/src/utils/tapped-stream.ts +0 -60
- package/src/worktree-manager.ts +0 -974
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import type { ContentBlock } from "@agentclientprotocol/sdk";
|
|
2
|
+
import { Saga } from "@posthog/shared";
|
|
3
|
+
import { POSTHOG_NOTIFICATIONS } from "../acp-extensions.js";
|
|
4
|
+
import type { PostHogAPIClient } from "../posthog-api.js";
|
|
5
|
+
import { TreeTracker } from "../tree-tracker.js";
|
|
6
|
+
import type {
|
|
7
|
+
DeviceInfo,
|
|
8
|
+
StoredNotification,
|
|
9
|
+
TreeSnapshotEvent,
|
|
10
|
+
} from "../types.js";
|
|
11
|
+
import { Logger } from "../utils/logger.js";
|
|
12
|
+
|
|
13
|
+
export interface ConversationTurn {
|
|
14
|
+
role: "user" | "assistant";
|
|
15
|
+
content: ContentBlock[];
|
|
16
|
+
toolCalls?: ToolCallInfo[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ToolCallInfo {
|
|
20
|
+
toolCallId: string;
|
|
21
|
+
toolName: string;
|
|
22
|
+
input: unknown;
|
|
23
|
+
result?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ResumeInput {
|
|
27
|
+
taskId: string;
|
|
28
|
+
runId: string;
|
|
29
|
+
repositoryPath: string;
|
|
30
|
+
apiClient: PostHogAPIClient;
|
|
31
|
+
logger?: Logger;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ResumeOutput {
|
|
35
|
+
conversation: ConversationTurn[];
|
|
36
|
+
latestSnapshot: TreeSnapshotEvent | null;
|
|
37
|
+
snapshotApplied: boolean;
|
|
38
|
+
interrupted: boolean;
|
|
39
|
+
lastDevice?: DeviceInfo;
|
|
40
|
+
logEntryCount: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
|
|
44
|
+
protected async execute(input: ResumeInput): Promise<ResumeOutput> {
|
|
45
|
+
const { taskId, runId, repositoryPath, apiClient } = input;
|
|
46
|
+
const logger =
|
|
47
|
+
input.logger || new Logger({ debug: false, prefix: "[Resume]" });
|
|
48
|
+
|
|
49
|
+
// Step 1: Fetch task run (read-only)
|
|
50
|
+
const taskRun = await this.readOnlyStep("fetch_task_run", () =>
|
|
51
|
+
apiClient.getTaskRun(taskId, runId),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (!taskRun.log_url) {
|
|
55
|
+
this.log.info("No log URL found, starting fresh");
|
|
56
|
+
return this.emptyResult();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Step 2: Fetch log entries (read-only)
|
|
60
|
+
const entries = await this.readOnlyStep("fetch_logs", () =>
|
|
61
|
+
apiClient.fetchTaskRunLogs(taskRun),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (entries.length === 0) {
|
|
65
|
+
this.log.info("No log entries found, starting fresh");
|
|
66
|
+
return this.emptyResult();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.log.info("Fetched log entries", { count: entries.length });
|
|
70
|
+
|
|
71
|
+
// Step 3: Find latest snapshot (read-only, pure computation)
|
|
72
|
+
const latestSnapshot = await this.readOnlyStep("find_snapshot", () =>
|
|
73
|
+
Promise.resolve(this.findLatestTreeSnapshot(entries)),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Step 4: Apply snapshot if present (wrapped in step for consistent logging)
|
|
77
|
+
// Note: We use a try/catch inside the step because snapshot failure should NOT fail the saga
|
|
78
|
+
let snapshotApplied = false;
|
|
79
|
+
if (latestSnapshot?.archiveUrl) {
|
|
80
|
+
this.log.info("Found tree snapshot", {
|
|
81
|
+
treeHash: latestSnapshot.treeHash,
|
|
82
|
+
hasArchiveUrl: true,
|
|
83
|
+
changes: latestSnapshot.changes?.length ?? 0,
|
|
84
|
+
interrupted: latestSnapshot.interrupted,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await this.step({
|
|
88
|
+
name: "apply_snapshot",
|
|
89
|
+
execute: async () => {
|
|
90
|
+
const treeTracker = new TreeTracker({
|
|
91
|
+
repositoryPath,
|
|
92
|
+
taskId,
|
|
93
|
+
runId,
|
|
94
|
+
apiClient,
|
|
95
|
+
logger: logger.child("TreeTracker"),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await treeTracker.applyTreeSnapshot(latestSnapshot);
|
|
100
|
+
treeTracker.setLastTreeHash(latestSnapshot.treeHash);
|
|
101
|
+
snapshotApplied = true;
|
|
102
|
+
this.log.info("Tree snapshot applied successfully", {
|
|
103
|
+
treeHash: latestSnapshot.treeHash,
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Log but don't fail - continue with conversation rebuild
|
|
107
|
+
// ApplySnapshotSaga handles its own rollback internally
|
|
108
|
+
this.log.warn(
|
|
109
|
+
"Failed to apply tree snapshot, continuing without it",
|
|
110
|
+
{
|
|
111
|
+
error: error instanceof Error ? error.message : String(error),
|
|
112
|
+
treeHash: latestSnapshot.treeHash,
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
rollback: async () => {
|
|
118
|
+
// Inner ApplySnapshotSaga handles its own rollback
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
} else if (latestSnapshot) {
|
|
122
|
+
this.log.warn(
|
|
123
|
+
"Snapshot found but has no archive URL - files cannot be restored",
|
|
124
|
+
{
|
|
125
|
+
treeHash: latestSnapshot.treeHash,
|
|
126
|
+
changes: latestSnapshot.changes?.length ?? 0,
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Step 5: Rebuild conversation (read-only, pure computation)
|
|
132
|
+
const conversation = await this.readOnlyStep("rebuild_conversation", () =>
|
|
133
|
+
Promise.resolve(this.rebuildConversation(entries)),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Step 6: Find device info (read-only, pure computation)
|
|
137
|
+
const lastDevice = await this.readOnlyStep("find_device", () =>
|
|
138
|
+
Promise.resolve(this.findLastDeviceInfo(entries)),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
this.log.info("Resume state rebuilt", {
|
|
142
|
+
turns: conversation.length,
|
|
143
|
+
hasSnapshot: !!latestSnapshot,
|
|
144
|
+
snapshotApplied,
|
|
145
|
+
interrupted: latestSnapshot?.interrupted ?? false,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
conversation,
|
|
150
|
+
latestSnapshot,
|
|
151
|
+
snapshotApplied,
|
|
152
|
+
interrupted: latestSnapshot?.interrupted ?? false,
|
|
153
|
+
lastDevice,
|
|
154
|
+
logEntryCount: entries.length,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private emptyResult(): ResumeOutput {
|
|
159
|
+
return {
|
|
160
|
+
conversation: [],
|
|
161
|
+
latestSnapshot: null,
|
|
162
|
+
snapshotApplied: false,
|
|
163
|
+
interrupted: false,
|
|
164
|
+
logEntryCount: 0,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private findLatestTreeSnapshot(
|
|
169
|
+
entries: StoredNotification[],
|
|
170
|
+
): TreeSnapshotEvent | null {
|
|
171
|
+
const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT}`;
|
|
172
|
+
|
|
173
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
174
|
+
const entry = entries[i];
|
|
175
|
+
const method = entry.notification?.method;
|
|
176
|
+
if (
|
|
177
|
+
method === sdkPrefixedMethod ||
|
|
178
|
+
method === POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT
|
|
179
|
+
) {
|
|
180
|
+
const params = entry.notification.params as
|
|
181
|
+
| TreeSnapshotEvent
|
|
182
|
+
| undefined;
|
|
183
|
+
if (params?.treeHash) {
|
|
184
|
+
return params;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private findLastDeviceInfo(
|
|
192
|
+
entries: StoredNotification[],
|
|
193
|
+
): DeviceInfo | undefined {
|
|
194
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
195
|
+
const entry = entries[i];
|
|
196
|
+
const params = entry.notification?.params as
|
|
197
|
+
| { device?: DeviceInfo }
|
|
198
|
+
| undefined;
|
|
199
|
+
if (params?.device) {
|
|
200
|
+
return params.device;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private rebuildConversation(
|
|
207
|
+
entries: StoredNotification[],
|
|
208
|
+
): ConversationTurn[] {
|
|
209
|
+
const turns: ConversationTurn[] = [];
|
|
210
|
+
let currentAssistantContent: ContentBlock[] = [];
|
|
211
|
+
let currentToolCalls: ToolCallInfo[] = [];
|
|
212
|
+
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
const method = entry.notification?.method;
|
|
215
|
+
const params = entry.notification?.params as Record<string, unknown>;
|
|
216
|
+
|
|
217
|
+
if (method === "session/update" && params?.update) {
|
|
218
|
+
const update = params.update as Record<string, unknown>;
|
|
219
|
+
const sessionUpdate = update.sessionUpdate as string;
|
|
220
|
+
|
|
221
|
+
switch (sessionUpdate) {
|
|
222
|
+
case "user_message":
|
|
223
|
+
case "user_message_chunk": {
|
|
224
|
+
if (
|
|
225
|
+
currentAssistantContent.length > 0 ||
|
|
226
|
+
currentToolCalls.length > 0
|
|
227
|
+
) {
|
|
228
|
+
turns.push({
|
|
229
|
+
role: "assistant",
|
|
230
|
+
content: currentAssistantContent,
|
|
231
|
+
toolCalls:
|
|
232
|
+
currentToolCalls.length > 0 ? currentToolCalls : undefined,
|
|
233
|
+
});
|
|
234
|
+
currentAssistantContent = [];
|
|
235
|
+
currentToolCalls = [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const content = update.content as ContentBlock | ContentBlock[];
|
|
239
|
+
const contentArray = Array.isArray(content) ? content : [content];
|
|
240
|
+
turns.push({
|
|
241
|
+
role: "user",
|
|
242
|
+
content: contentArray,
|
|
243
|
+
});
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
case "agent_message_chunk": {
|
|
248
|
+
const content = update.content as ContentBlock | undefined;
|
|
249
|
+
if (content) {
|
|
250
|
+
if (
|
|
251
|
+
content.type === "text" &&
|
|
252
|
+
currentAssistantContent.length > 0 &&
|
|
253
|
+
currentAssistantContent[currentAssistantContent.length - 1]
|
|
254
|
+
.type === "text"
|
|
255
|
+
) {
|
|
256
|
+
const lastBlock = currentAssistantContent[
|
|
257
|
+
currentAssistantContent.length - 1
|
|
258
|
+
] as { type: "text"; text: string };
|
|
259
|
+
lastBlock.text += (
|
|
260
|
+
content as { type: "text"; text: string }
|
|
261
|
+
).text;
|
|
262
|
+
} else {
|
|
263
|
+
currentAssistantContent.push(content);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case "tool_call":
|
|
270
|
+
case "tool_call_update": {
|
|
271
|
+
const meta = (update._meta as Record<string, unknown>)
|
|
272
|
+
?.claudeCode as Record<string, unknown> | undefined;
|
|
273
|
+
if (meta) {
|
|
274
|
+
const toolCallId = meta.toolCallId as string | undefined;
|
|
275
|
+
const toolName = meta.toolName as string | undefined;
|
|
276
|
+
const toolInput = meta.toolInput;
|
|
277
|
+
const toolResponse = meta.toolResponse;
|
|
278
|
+
|
|
279
|
+
if (toolCallId && toolName) {
|
|
280
|
+
let toolCall = currentToolCalls.find(
|
|
281
|
+
(tc) => tc.toolCallId === toolCallId,
|
|
282
|
+
);
|
|
283
|
+
if (!toolCall) {
|
|
284
|
+
toolCall = {
|
|
285
|
+
toolCallId,
|
|
286
|
+
toolName,
|
|
287
|
+
input: toolInput,
|
|
288
|
+
};
|
|
289
|
+
currentToolCalls.push(toolCall);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (toolResponse !== undefined) {
|
|
293
|
+
toolCall.result = toolResponse;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
case "tool_result": {
|
|
301
|
+
const meta = (update._meta as Record<string, unknown>)
|
|
302
|
+
?.claudeCode as Record<string, unknown> | undefined;
|
|
303
|
+
if (meta) {
|
|
304
|
+
const toolCallId = meta.toolCallId as string | undefined;
|
|
305
|
+
const toolResponse = meta.toolResponse;
|
|
306
|
+
|
|
307
|
+
if (toolCallId) {
|
|
308
|
+
const toolCall = currentToolCalls.find(
|
|
309
|
+
(tc) => tc.toolCallId === toolCallId,
|
|
310
|
+
);
|
|
311
|
+
if (toolCall && toolResponse !== undefined) {
|
|
312
|
+
toolCall.result = toolResponse;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
|
|
323
|
+
turns.push({
|
|
324
|
+
role: "assistant",
|
|
325
|
+
content: currentAssistantContent,
|
|
326
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return turns;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import type { SagaLogger } from "@posthog/shared";
|
|
8
|
+
import * as tar from "tar";
|
|
9
|
+
import { vi } from "vitest";
|
|
10
|
+
import { POSTHOG_NOTIFICATIONS } from "../acp-extensions.js";
|
|
11
|
+
import type { PostHogAPIClient } from "../posthog-api.js";
|
|
12
|
+
import type { StoredNotification, TaskRun, TreeSnapshot } from "../types.js";
|
|
13
|
+
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
|
|
16
|
+
export interface TestRepo {
|
|
17
|
+
path: string;
|
|
18
|
+
cleanup: () => Promise<void>;
|
|
19
|
+
git: (args: string[]) => Promise<string>;
|
|
20
|
+
writeFile: (relativePath: string, content: string) => Promise<void>;
|
|
21
|
+
readFile: (relativePath: string) => Promise<string>;
|
|
22
|
+
deleteFile: (relativePath: string) => Promise<void>;
|
|
23
|
+
exists: (relativePath: string) => boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function createTestRepo(prefix = "test-repo"): Promise<TestRepo> {
|
|
27
|
+
const repoPath = join(
|
|
28
|
+
tmpdir(),
|
|
29
|
+
`${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
30
|
+
);
|
|
31
|
+
await mkdir(repoPath, { recursive: true });
|
|
32
|
+
|
|
33
|
+
const git = async (args: string[]): Promise<string> => {
|
|
34
|
+
const { stdout } = await execFileAsync("git", args, { cwd: repoPath });
|
|
35
|
+
return stdout.trim();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
await git(["init"]);
|
|
39
|
+
await git(["config", "user.email", "test@test.com"]);
|
|
40
|
+
await git(["config", "user.name", "Test"]);
|
|
41
|
+
|
|
42
|
+
await writeFile(join(repoPath, ".gitignore"), ".posthog/\n");
|
|
43
|
+
await writeFile(join(repoPath, "README.md"), "# Test Repo");
|
|
44
|
+
await git(["add", "."]);
|
|
45
|
+
await git(["commit", "-m", "Initial commit"]);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
path: repoPath,
|
|
49
|
+
cleanup: () => rm(repoPath, { recursive: true, force: true }),
|
|
50
|
+
git,
|
|
51
|
+
writeFile: async (relativePath: string, content: string) => {
|
|
52
|
+
const fullPath = join(repoPath, relativePath);
|
|
53
|
+
const dir = join(fullPath, "..");
|
|
54
|
+
await mkdir(dir, { recursive: true });
|
|
55
|
+
await writeFile(fullPath, content);
|
|
56
|
+
},
|
|
57
|
+
readFile: async (relativePath: string) => {
|
|
58
|
+
return readFile(join(repoPath, relativePath), "utf-8");
|
|
59
|
+
},
|
|
60
|
+
deleteFile: async (relativePath: string) => {
|
|
61
|
+
await rm(join(repoPath, relativePath), { force: true });
|
|
62
|
+
},
|
|
63
|
+
exists: (relativePath: string) => {
|
|
64
|
+
return existsSync(join(repoPath, relativePath));
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createMockLogger(): SagaLogger {
|
|
70
|
+
return {
|
|
71
|
+
info: vi.fn(),
|
|
72
|
+
debug: vi.fn(),
|
|
73
|
+
error: vi.fn(),
|
|
74
|
+
warn: vi.fn(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createMockApiClient(
|
|
79
|
+
overrides: Partial<PostHogAPIClient> = {},
|
|
80
|
+
): PostHogAPIClient {
|
|
81
|
+
return {
|
|
82
|
+
uploadTaskArtifacts: vi
|
|
83
|
+
.fn()
|
|
84
|
+
.mockResolvedValue([{ storage_path: "gs://bucket/trees/test.tar.gz" }]),
|
|
85
|
+
downloadArtifact: vi.fn(),
|
|
86
|
+
getTaskRun: vi.fn(),
|
|
87
|
+
fetchTaskRunLogs: vi.fn(),
|
|
88
|
+
...overrides,
|
|
89
|
+
} as unknown as PostHogAPIClient;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ArchiveFile {
|
|
93
|
+
path: string;
|
|
94
|
+
content: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ArchiveSymlink {
|
|
98
|
+
path: string;
|
|
99
|
+
target: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function createArchiveBuffer(
|
|
103
|
+
files: Array<ArchiveFile>,
|
|
104
|
+
symlinks: Array<ArchiveSymlink> = [],
|
|
105
|
+
): Promise<Buffer> {
|
|
106
|
+
const { symlink } = await import("node:fs/promises");
|
|
107
|
+
const tmpDir = join(
|
|
108
|
+
tmpdir(),
|
|
109
|
+
`archive-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
110
|
+
);
|
|
111
|
+
await mkdir(tmpDir, { recursive: true });
|
|
112
|
+
|
|
113
|
+
const filesToArchive =
|
|
114
|
+
files.length > 0 ? files : [{ path: ".empty", content: "" }];
|
|
115
|
+
|
|
116
|
+
for (const file of filesToArchive) {
|
|
117
|
+
const fullPath = join(tmpDir, file.path);
|
|
118
|
+
await mkdir(join(fullPath, ".."), { recursive: true });
|
|
119
|
+
await writeFile(fullPath, file.content);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const symlinkPaths: string[] = [];
|
|
123
|
+
for (const link of symlinks) {
|
|
124
|
+
const fullPath = join(tmpDir, link.path);
|
|
125
|
+
await mkdir(join(fullPath, ".."), { recursive: true });
|
|
126
|
+
await symlink(link.target, fullPath);
|
|
127
|
+
symlinkPaths.push(link.path);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const archivePath = join(tmpDir, "archive.tar.gz");
|
|
131
|
+
await tar.create({ gzip: true, file: archivePath, cwd: tmpDir }, [
|
|
132
|
+
...filesToArchive.map((f) => f.path),
|
|
133
|
+
...symlinkPaths,
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
const content = await readFile(archivePath);
|
|
137
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
138
|
+
|
|
139
|
+
return Buffer.from(content.toString("base64"));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createSnapshot(
|
|
143
|
+
overrides: Partial<TreeSnapshot> = {},
|
|
144
|
+
): TreeSnapshot {
|
|
145
|
+
return {
|
|
146
|
+
treeHash: "test-tree-hash",
|
|
147
|
+
baseCommit: null,
|
|
148
|
+
archiveUrl: "gs://bucket/trees/test.tar.gz",
|
|
149
|
+
changes: [],
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
...overrides,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createTaskRun(overrides: Partial<TaskRun> = {}): TaskRun {
|
|
156
|
+
return {
|
|
157
|
+
id: "run-1",
|
|
158
|
+
task: "task-1",
|
|
159
|
+
team: 1,
|
|
160
|
+
branch: null,
|
|
161
|
+
stage: null,
|
|
162
|
+
environment: "local",
|
|
163
|
+
status: "in_progress",
|
|
164
|
+
log_url: "https://logs.example.com/run-1",
|
|
165
|
+
error_message: null,
|
|
166
|
+
output: null,
|
|
167
|
+
state: {},
|
|
168
|
+
created_at: new Date().toISOString(),
|
|
169
|
+
updated_at: new Date().toISOString(),
|
|
170
|
+
completed_at: null,
|
|
171
|
+
...overrides,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function createNotification(
|
|
176
|
+
method: string,
|
|
177
|
+
params: Record<string, unknown>,
|
|
178
|
+
): StoredNotification {
|
|
179
|
+
return {
|
|
180
|
+
type: "notification",
|
|
181
|
+
timestamp: new Date().toISOString(),
|
|
182
|
+
notification: {
|
|
183
|
+
jsonrpc: "2.0",
|
|
184
|
+
method,
|
|
185
|
+
params,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function createUserMessage(content: string): StoredNotification {
|
|
191
|
+
return createNotification("session/update", {
|
|
192
|
+
update: {
|
|
193
|
+
sessionUpdate: "user_message",
|
|
194
|
+
content: { type: "text", text: content },
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function createAgentChunk(text: string): StoredNotification {
|
|
200
|
+
return createNotification("session/update", {
|
|
201
|
+
update: {
|
|
202
|
+
sessionUpdate: "agent_message_chunk",
|
|
203
|
+
content: { type: "text", text },
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function createToolCall(
|
|
209
|
+
toolCallId: string,
|
|
210
|
+
toolName: string,
|
|
211
|
+
toolInput: unknown,
|
|
212
|
+
): StoredNotification {
|
|
213
|
+
return createNotification("session/update", {
|
|
214
|
+
update: {
|
|
215
|
+
sessionUpdate: "tool_call",
|
|
216
|
+
_meta: {
|
|
217
|
+
claudeCode: { toolCallId, toolName, toolInput },
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function createToolResult(
|
|
224
|
+
toolCallId: string,
|
|
225
|
+
toolResponse: unknown,
|
|
226
|
+
): StoredNotification {
|
|
227
|
+
return createNotification("session/update", {
|
|
228
|
+
update: {
|
|
229
|
+
sessionUpdate: "tool_result",
|
|
230
|
+
_meta: {
|
|
231
|
+
claudeCode: { toolCallId, toolResponse },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function createTreeSnapshotNotification(
|
|
238
|
+
treeHash: string,
|
|
239
|
+
archiveUrl?: string,
|
|
240
|
+
options: { interrupted?: boolean; device?: { type: "local" | "cloud" } } = {},
|
|
241
|
+
): StoredNotification {
|
|
242
|
+
return createNotification(POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT, {
|
|
243
|
+
treeHash,
|
|
244
|
+
baseCommit: "abc123",
|
|
245
|
+
archiveUrl,
|
|
246
|
+
changes: [{ path: "file.ts", status: "A" }],
|
|
247
|
+
timestamp: new Date().toISOString(),
|
|
248
|
+
...options,
|
|
249
|
+
});
|
|
250
|
+
}
|