@posthog/agent 2.0.0 → 2.0.1

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.
Files changed (131) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +221 -219
  3. package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
  4. package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
  5. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
  6. package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
  7. package/dist/adapters/claude/permissions/permission-options.js +117 -0
  8. package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
  9. package/dist/adapters/claude/questions/utils.d.ts +132 -0
  10. package/dist/adapters/claude/questions/utils.js +63 -0
  11. package/dist/adapters/claude/questions/utils.js.map +1 -0
  12. package/dist/adapters/claude/tools.d.ts +18 -0
  13. package/dist/adapters/claude/tools.js +95 -0
  14. package/dist/adapters/claude/tools.js.map +1 -0
  15. package/dist/agent-DBQY1BfC.d.ts +123 -0
  16. package/dist/agent.d.ts +5 -0
  17. package/dist/agent.js +3656 -0
  18. package/dist/agent.js.map +1 -0
  19. package/dist/claude-cli/cli.js +3695 -2746
  20. package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
  21. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
  22. package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
  23. package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
  24. package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
  25. package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
  26. package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
  27. package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
  28. package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
  29. package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
  30. package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
  31. package/dist/gateway-models.d.ts +24 -0
  32. package/dist/gateway-models.js +93 -0
  33. package/dist/gateway-models.js.map +1 -0
  34. package/dist/index.d.ts +170 -1157
  35. package/dist/index.js +3252 -5074
  36. package/dist/index.js.map +1 -1
  37. package/dist/logger-DDBiMOOD.d.ts +24 -0
  38. package/dist/posthog-api.d.ts +40 -0
  39. package/dist/posthog-api.js +175 -0
  40. package/dist/posthog-api.js.map +1 -0
  41. package/dist/server/agent-server.d.ts +41 -0
  42. package/dist/server/agent-server.js +4451 -0
  43. package/dist/server/agent-server.js.map +1 -0
  44. package/dist/server/bin.d.ts +1 -0
  45. package/dist/server/bin.js +4507 -0
  46. package/dist/server/bin.js.map +1 -0
  47. package/dist/types.d.ts +129 -0
  48. package/dist/types.js +1 -0
  49. package/dist/types.js.map +1 -0
  50. package/package.json +66 -14
  51. package/src/acp-extensions.ts +98 -16
  52. package/src/adapters/acp-connection.ts +494 -0
  53. package/src/adapters/base-acp-agent.ts +150 -0
  54. package/src/adapters/claude/claude-agent.ts +596 -0
  55. package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
  56. package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
  57. package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
  58. package/src/adapters/claude/hooks.ts +64 -0
  59. package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
  60. package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
  61. package/src/adapters/claude/permissions/permission-options.ts +103 -0
  62. package/src/adapters/claude/plan/utils.ts +56 -0
  63. package/src/adapters/claude/questions/utils.ts +92 -0
  64. package/src/adapters/claude/session/commands.ts +38 -0
  65. package/src/adapters/claude/session/mcp-config.ts +37 -0
  66. package/src/adapters/claude/session/models.ts +12 -0
  67. package/src/adapters/claude/session/options.ts +236 -0
  68. package/src/adapters/claude/tool-meta.ts +143 -0
  69. package/src/adapters/claude/tools.ts +53 -688
  70. package/src/adapters/claude/types.ts +61 -0
  71. package/src/adapters/codex/spawn.ts +130 -0
  72. package/src/agent.ts +96 -587
  73. package/src/execution-mode.ts +43 -0
  74. package/src/gateway-models.ts +135 -0
  75. package/src/index.ts +79 -0
  76. package/src/otel-log-writer.test.ts +105 -0
  77. package/src/otel-log-writer.ts +94 -0
  78. package/src/posthog-api.ts +75 -235
  79. package/src/resume.ts +115 -0
  80. package/src/sagas/apply-snapshot-saga.test.ts +690 -0
  81. package/src/sagas/apply-snapshot-saga.ts +88 -0
  82. package/src/sagas/capture-tree-saga.test.ts +892 -0
  83. package/src/sagas/capture-tree-saga.ts +141 -0
  84. package/src/sagas/resume-saga.test.ts +558 -0
  85. package/src/sagas/resume-saga.ts +332 -0
  86. package/src/sagas/test-fixtures.ts +250 -0
  87. package/src/server/agent-server.test.ts +220 -0
  88. package/src/server/agent-server.ts +748 -0
  89. package/src/server/bin.ts +88 -0
  90. package/src/server/jwt.ts +65 -0
  91. package/src/server/schemas.ts +47 -0
  92. package/src/server/types.ts +13 -0
  93. package/src/server/utils/retry.test.ts +122 -0
  94. package/src/server/utils/retry.ts +61 -0
  95. package/src/server/utils/sse-parser.test.ts +93 -0
  96. package/src/server/utils/sse-parser.ts +46 -0
  97. package/src/session-log-writer.test.ts +140 -0
  98. package/src/session-log-writer.ts +137 -0
  99. package/src/test/assertions.ts +114 -0
  100. package/src/test/controllers/sse-controller.ts +107 -0
  101. package/src/test/fixtures/api.ts +111 -0
  102. package/src/test/fixtures/config.ts +33 -0
  103. package/src/test/fixtures/notifications.ts +92 -0
  104. package/src/test/mocks/claude-sdk.ts +251 -0
  105. package/src/test/mocks/msw-handlers.ts +48 -0
  106. package/src/test/setup.ts +114 -0
  107. package/src/test/wait.ts +41 -0
  108. package/src/tree-tracker.ts +173 -0
  109. package/src/types.ts +54 -137
  110. package/src/utils/acp-content.ts +58 -0
  111. package/src/utils/async-mutex.test.ts +104 -0
  112. package/src/utils/async-mutex.ts +31 -0
  113. package/src/utils/common.ts +15 -0
  114. package/src/utils/gateway.ts +9 -6
  115. package/src/utils/logger.ts +0 -30
  116. package/src/utils/streams.ts +220 -0
  117. package/CLAUDE.md +0 -331
  118. package/src/adapters/claude/claude.ts +0 -1947
  119. package/src/adapters/claude/mcp-server.ts +0 -810
  120. package/src/adapters/claude/utils.ts +0 -267
  121. package/src/adapters/connection.ts +0 -95
  122. package/src/file-manager.ts +0 -273
  123. package/src/git-manager.ts +0 -577
  124. package/src/schemas.ts +0 -241
  125. package/src/session-store.ts +0 -259
  126. package/src/task-manager.ts +0 -163
  127. package/src/todo-manager.ts +0 -180
  128. package/src/tools/registry.ts +0 -134
  129. package/src/tools/types.ts +0 -133
  130. package/src/utils/tapped-stream.ts +0 -60
  131. package/src/worktree-manager.ts +0 -974
@@ -0,0 +1,748 @@
1
+ import {
2
+ ClientSideConnection,
3
+ ndJsonStream,
4
+ PROTOCOL_VERSION,
5
+ } from "@agentclientprotocol/sdk";
6
+ import { type ServerType, serve } from "@hono/node-server";
7
+ import { Hono } from "hono";
8
+ import { POSTHOG_NOTIFICATIONS } from "../acp-extensions.js";
9
+ import {
10
+ createAcpConnection,
11
+ type InProcessAcpConnection,
12
+ } from "../adapters/acp-connection.js";
13
+ import { PostHogAPIClient } from "../posthog-api.js";
14
+ import { SessionLogWriter } from "../session-log-writer.js";
15
+ import { TreeTracker } from "../tree-tracker.js";
16
+ import type { AgentMode, DeviceInfo, TreeSnapshotEvent } from "../types.js";
17
+ import { AsyncMutex } from "../utils/async-mutex.js";
18
+ import { getLlmGatewayUrl } from "../utils/gateway.js";
19
+ import { Logger } from "../utils/logger.js";
20
+ import { type JwtPayload, JwtValidationError, validateJwt } from "./jwt.js";
21
+ import { jsonRpcRequestSchema, validateCommandParams } from "./schemas.js";
22
+ import type { AgentServerConfig } from "./types.js";
23
+
24
+ type MessageCallback = (message: unknown) => void;
25
+
26
+ class NdJsonTap {
27
+ private decoder = new TextDecoder();
28
+ private buffer = "";
29
+
30
+ constructor(private onMessage: MessageCallback) {}
31
+
32
+ process(chunk: Uint8Array): void {
33
+ this.buffer += this.decoder.decode(chunk, { stream: true });
34
+ const lines = this.buffer.split("\n");
35
+ this.buffer = lines.pop() ?? "";
36
+
37
+ for (const line of lines) {
38
+ if (!line.trim()) continue;
39
+ try {
40
+ this.onMessage(JSON.parse(line));
41
+ } catch {
42
+ // Not valid JSON, skip
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ function createTappedReadableStream(
49
+ underlying: ReadableStream<Uint8Array>,
50
+ onMessage: MessageCallback,
51
+ logger?: Logger,
52
+ ): ReadableStream<Uint8Array> {
53
+ const reader = underlying.getReader();
54
+ const tap = new NdJsonTap(onMessage);
55
+
56
+ return new ReadableStream<Uint8Array>({
57
+ async pull(controller) {
58
+ try {
59
+ const { value, done } = await reader.read();
60
+ if (done) {
61
+ controller.close();
62
+ return;
63
+ }
64
+ tap.process(value);
65
+ controller.enqueue(value);
66
+ } catch (error) {
67
+ logger?.debug("Read failed, closing stream", error);
68
+ controller.close();
69
+ }
70
+ },
71
+ cancel() {
72
+ reader.releaseLock();
73
+ },
74
+ });
75
+ }
76
+
77
+ function createTappedWritableStream(
78
+ underlying: WritableStream<Uint8Array>,
79
+ onMessage: MessageCallback,
80
+ logger?: Logger,
81
+ ): WritableStream<Uint8Array> {
82
+ const tap = new NdJsonTap(onMessage);
83
+ const mutex = new AsyncMutex();
84
+
85
+ return new WritableStream<Uint8Array>({
86
+ async write(chunk) {
87
+ tap.process(chunk);
88
+ await mutex.acquire();
89
+ try {
90
+ const writer = underlying.getWriter();
91
+ await writer.write(chunk);
92
+ writer.releaseLock();
93
+ } catch (error) {
94
+ logger?.debug("Write failed (stream may be closed)", error);
95
+ } finally {
96
+ mutex.release();
97
+ }
98
+ },
99
+ async close() {
100
+ await mutex.acquire();
101
+ try {
102
+ const writer = underlying.getWriter();
103
+ await writer.close();
104
+ writer.releaseLock();
105
+ } catch (error) {
106
+ logger?.debug("Close failed (stream may be closed)", error);
107
+ } finally {
108
+ mutex.release();
109
+ }
110
+ },
111
+ async abort(reason) {
112
+ await mutex.acquire();
113
+ try {
114
+ const writer = underlying.getWriter();
115
+ await writer.abort(reason);
116
+ writer.releaseLock();
117
+ } catch (error) {
118
+ logger?.debug("Abort failed (stream may be closed)", error);
119
+ } finally {
120
+ mutex.release();
121
+ }
122
+ },
123
+ });
124
+ }
125
+
126
+ interface SseController {
127
+ send: (data: unknown) => void;
128
+ close: () => void;
129
+ }
130
+
131
+ interface ActiveSession {
132
+ payload: JwtPayload;
133
+ acpConnection: InProcessAcpConnection;
134
+ clientConnection: ClientSideConnection;
135
+ treeTracker: TreeTracker;
136
+ sseController: SseController | null;
137
+ deviceInfo: DeviceInfo;
138
+ logWriter: SessionLogWriter;
139
+ }
140
+
141
+ export class AgentServer {
142
+ private config: AgentServerConfig;
143
+ private logger: Logger;
144
+ private server: ServerType | null = null;
145
+ private session: ActiveSession | null = null;
146
+ private app: Hono;
147
+ private posthogAPI: PostHogAPIClient;
148
+
149
+ constructor(config: AgentServerConfig) {
150
+ this.config = config;
151
+ this.logger = new Logger({ debug: true, prefix: "[AgentServer]" });
152
+ this.posthogAPI = new PostHogAPIClient({
153
+ apiUrl: config.apiUrl,
154
+ projectId: config.projectId,
155
+ getApiKey: () => config.apiKey,
156
+ });
157
+ this.app = this.createApp();
158
+ }
159
+
160
+ private getEffectiveMode(payload: JwtPayload): AgentMode {
161
+ return payload.mode ?? this.config.mode;
162
+ }
163
+
164
+ private createApp(): Hono {
165
+ const app = new Hono();
166
+
167
+ app.get("/health", (c) => {
168
+ return c.json({ status: "ok", hasSession: !!this.session });
169
+ });
170
+
171
+ app.get("/events", async (c) => {
172
+ let payload: JwtPayload;
173
+
174
+ try {
175
+ payload = this.authenticateRequest(c.req.header.bind(c.req));
176
+ } catch (error) {
177
+ return c.json(
178
+ {
179
+ error:
180
+ error instanceof JwtValidationError
181
+ ? error.message
182
+ : "Invalid token",
183
+ code:
184
+ error instanceof JwtValidationError
185
+ ? error.code
186
+ : "invalid_token",
187
+ },
188
+ 401,
189
+ );
190
+ }
191
+
192
+ const stream = new ReadableStream({
193
+ start: async (controller) => {
194
+ const sseController: SseController = {
195
+ send: (data: unknown) => {
196
+ try {
197
+ controller.enqueue(
198
+ new TextEncoder().encode(`data: ${JSON.stringify(data)}\n\n`),
199
+ );
200
+ } catch (error) {
201
+ this.logger.debug(
202
+ "SSE send failed (stream may be closed)",
203
+ error,
204
+ );
205
+ }
206
+ },
207
+ close: () => {
208
+ try {
209
+ controller.close();
210
+ } catch (error) {
211
+ this.logger.debug("SSE close failed (already closed)", error);
212
+ }
213
+ },
214
+ };
215
+
216
+ if (!this.session || this.session.payload.run_id !== payload.run_id) {
217
+ await this.initializeSession(payload, sseController);
218
+ } else {
219
+ this.session.sseController = sseController;
220
+ }
221
+
222
+ this.sendSseEvent(sseController, {
223
+ type: "connected",
224
+ run_id: payload.run_id,
225
+ });
226
+ },
227
+ cancel: () => {
228
+ this.logger.info("SSE connection closed");
229
+ if (this.session?.sseController) {
230
+ this.session.sseController = null;
231
+ }
232
+ },
233
+ });
234
+
235
+ return new Response(stream, {
236
+ headers: {
237
+ "Content-Type": "text/event-stream",
238
+ "Cache-Control": "no-cache",
239
+ Connection: "keep-alive",
240
+ },
241
+ });
242
+ });
243
+
244
+ app.post("/command", async (c) => {
245
+ let payload: JwtPayload;
246
+
247
+ try {
248
+ payload = this.authenticateRequest(c.req.header.bind(c.req));
249
+ } catch (error) {
250
+ return c.json(
251
+ {
252
+ error:
253
+ error instanceof JwtValidationError
254
+ ? error.message
255
+ : "Invalid token",
256
+ },
257
+ 401,
258
+ );
259
+ }
260
+
261
+ if (!this.session || this.session.payload.run_id !== payload.run_id) {
262
+ return c.json({ error: "No active session for this run" }, 400);
263
+ }
264
+
265
+ const rawBody = await c.req.json().catch(() => null);
266
+ const parseResult = jsonRpcRequestSchema.safeParse(rawBody);
267
+
268
+ if (!parseResult.success) {
269
+ return c.json({ error: "Invalid JSON-RPC request" }, 400);
270
+ }
271
+
272
+ const command = parseResult.data;
273
+ const paramsValidation = validateCommandParams(
274
+ command.method,
275
+ command.params ?? {},
276
+ );
277
+
278
+ if (!paramsValidation.success) {
279
+ return c.json(
280
+ {
281
+ jsonrpc: "2.0",
282
+ id: command.id,
283
+ error: {
284
+ code: -32602,
285
+ message: paramsValidation.error,
286
+ },
287
+ },
288
+ 200,
289
+ );
290
+ }
291
+
292
+ try {
293
+ const result = await this.executeCommand(
294
+ command.method,
295
+ (command.params as Record<string, unknown>) || {},
296
+ );
297
+ return c.json({
298
+ jsonrpc: "2.0",
299
+ id: command.id,
300
+ result,
301
+ });
302
+ } catch (error) {
303
+ return c.json({
304
+ jsonrpc: "2.0",
305
+ id: command.id,
306
+ error: {
307
+ code: -32000,
308
+ message: error instanceof Error ? error.message : "Unknown error",
309
+ },
310
+ });
311
+ }
312
+ });
313
+
314
+ app.notFound((c) => {
315
+ return c.json({ error: "Not found" }, 404);
316
+ });
317
+
318
+ return app;
319
+ }
320
+
321
+ async start(): Promise<void> {
322
+ await new Promise<void>((resolve) => {
323
+ this.server = serve(
324
+ {
325
+ fetch: this.app.fetch,
326
+ port: this.config.port,
327
+ },
328
+ () => {
329
+ this.logger.info(`HTTP server listening on port ${this.config.port}`);
330
+ resolve();
331
+ },
332
+ );
333
+ });
334
+
335
+ await this.autoInitializeSession();
336
+ }
337
+
338
+ private async autoInitializeSession(): Promise<void> {
339
+ const { taskId, runId, mode, projectId } = this.config;
340
+
341
+ this.logger.info("Auto-initializing session", { taskId, runId, mode });
342
+
343
+ // Create a synthetic payload from config (no JWT needed for auto-init)
344
+ const payload: JwtPayload = {
345
+ task_id: taskId,
346
+ run_id: runId,
347
+ team_id: projectId,
348
+ user_id: 0, // System-initiated
349
+ distinct_id: "agent-server",
350
+ mode,
351
+ };
352
+
353
+ await this.initializeSession(payload, null);
354
+ }
355
+
356
+ async stop(): Promise<void> {
357
+ this.logger.info("Stopping agent server...");
358
+
359
+ if (this.session) {
360
+ await this.cleanupSession();
361
+ }
362
+
363
+ if (this.server) {
364
+ this.server.close();
365
+ this.server = null;
366
+ }
367
+
368
+ this.logger.info("Agent server stopped");
369
+ }
370
+
371
+ private authenticateRequest(
372
+ getHeader: (name: string) => string | undefined,
373
+ ): JwtPayload {
374
+ // Always require JWT validation - never trust unverified headers
375
+ if (!this.config.jwtPublicKey) {
376
+ throw new JwtValidationError(
377
+ "Server not configured with JWT public key",
378
+ "server_error",
379
+ );
380
+ }
381
+
382
+ const authHeader = getHeader("authorization");
383
+ if (!authHeader?.startsWith("Bearer ")) {
384
+ throw new JwtValidationError(
385
+ "Missing authorization header",
386
+ "invalid_token",
387
+ );
388
+ }
389
+
390
+ const token = authHeader.slice(7);
391
+ return validateJwt(token, this.config.jwtPublicKey);
392
+ }
393
+
394
+ private async executeCommand(
395
+ method: string,
396
+ params: Record<string, unknown>,
397
+ ): Promise<unknown> {
398
+ if (!this.session) {
399
+ throw new Error("No active session");
400
+ }
401
+
402
+ switch (method) {
403
+ case POSTHOG_NOTIFICATIONS.USER_MESSAGE:
404
+ case "user_message": {
405
+ const content = params.content as string;
406
+
407
+ this.logger.info(
408
+ `Processing user message: ${content.substring(0, 100)}...`,
409
+ );
410
+
411
+ const result = await this.session.clientConnection.prompt({
412
+ sessionId: this.session.payload.run_id,
413
+ prompt: [{ type: "text", text: content }],
414
+ });
415
+
416
+ return { stopReason: result.stopReason };
417
+ }
418
+
419
+ case POSTHOG_NOTIFICATIONS.CANCEL:
420
+ case "cancel": {
421
+ this.logger.info("Cancel requested");
422
+ await this.session.clientConnection.cancel({
423
+ sessionId: this.session.payload.run_id,
424
+ });
425
+ return { cancelled: true };
426
+ }
427
+
428
+ case POSTHOG_NOTIFICATIONS.CLOSE:
429
+ case "close": {
430
+ this.logger.info("Close requested");
431
+ await this.cleanupSession();
432
+ return { closed: true };
433
+ }
434
+
435
+ default:
436
+ throw new Error(`Unknown method: ${method}`);
437
+ }
438
+ }
439
+
440
+ private async initializeSession(
441
+ payload: JwtPayload,
442
+ sseController: SseController | null,
443
+ ): Promise<void> {
444
+ if (this.session) {
445
+ await this.cleanupSession();
446
+ }
447
+
448
+ this.logger.info("Initializing session", {
449
+ runId: payload.run_id,
450
+ taskId: payload.task_id,
451
+ });
452
+
453
+ const deviceInfo: DeviceInfo = {
454
+ type: "cloud",
455
+ name: process.env.HOSTNAME || "cloud-sandbox",
456
+ };
457
+
458
+ this.configureEnvironment();
459
+
460
+ const treeTracker = new TreeTracker({
461
+ repositoryPath: this.config.repositoryPath,
462
+ taskId: payload.task_id,
463
+ runId: payload.run_id,
464
+ logger: new Logger({ debug: true, prefix: "[TreeTracker]" }),
465
+ });
466
+
467
+ const _posthogAPI = new PostHogAPIClient({
468
+ apiUrl: this.config.apiUrl,
469
+ projectId: this.config.projectId,
470
+ getApiKey: () => this.config.apiKey,
471
+ });
472
+
473
+ const logWriter = new SessionLogWriter({
474
+ otelConfig: {
475
+ posthogHost: this.config.apiUrl,
476
+ apiKey: this.config.apiKey,
477
+ logsPath: "/i/v1/agent-logs",
478
+ },
479
+ logger: new Logger({ debug: true, prefix: "[SessionLogWriter]" }),
480
+ });
481
+
482
+ const acpConnection = createAcpConnection({
483
+ taskRunId: payload.run_id,
484
+ taskId: payload.task_id,
485
+ deviceType: deviceInfo.type,
486
+ logWriter,
487
+ });
488
+
489
+ // Tap both streams to broadcast all ACP messages via SSE (mimics local transport)
490
+ const onAcpMessage = (message: unknown) => {
491
+ this.broadcastEvent({
492
+ type: "notification",
493
+ timestamp: new Date().toISOString(),
494
+ notification: message,
495
+ });
496
+ };
497
+
498
+ const tappedReadable = createTappedReadableStream(
499
+ acpConnection.clientStreams.readable as ReadableStream<Uint8Array>,
500
+ onAcpMessage,
501
+ this.logger,
502
+ );
503
+
504
+ const tappedWritable = createTappedWritableStream(
505
+ acpConnection.clientStreams.writable as WritableStream<Uint8Array>,
506
+ onAcpMessage,
507
+ this.logger,
508
+ );
509
+
510
+ const clientStream = ndJsonStream(tappedWritable, tappedReadable);
511
+
512
+ const clientConnection = new ClientSideConnection(
513
+ () => this.createCloudClient(payload),
514
+ clientStream,
515
+ );
516
+
517
+ await clientConnection.initialize({
518
+ protocolVersion: PROTOCOL_VERSION,
519
+ clientCapabilities: {},
520
+ });
521
+
522
+ await clientConnection.newSession({
523
+ cwd: this.config.repositoryPath,
524
+ mcpServers: [],
525
+ _meta: { sessionId: payload.run_id },
526
+ });
527
+
528
+ this.session = {
529
+ payload,
530
+ acpConnection,
531
+ clientConnection,
532
+ treeTracker,
533
+ sseController,
534
+ deviceInfo,
535
+ logWriter,
536
+ };
537
+
538
+ this.logger.info("Session initialized successfully");
539
+
540
+ await this.sendInitialTaskMessage(payload);
541
+ }
542
+
543
+ private async sendInitialTaskMessage(payload: JwtPayload): Promise<void> {
544
+ if (!this.session) return;
545
+
546
+ try {
547
+ this.logger.info("Fetching task details", { taskId: payload.task_id });
548
+ const task = await this.posthogAPI.getTask(payload.task_id);
549
+
550
+ if (!task.description) {
551
+ this.logger.warn("Task has no description, skipping initial message");
552
+ return;
553
+ }
554
+
555
+ this.logger.info("Sending initial task message", {
556
+ taskId: payload.task_id,
557
+ descriptionLength: task.description.length,
558
+ });
559
+
560
+ const result = await this.session.clientConnection.prompt({
561
+ sessionId: payload.run_id,
562
+ prompt: [{ type: "text", text: task.description }],
563
+ });
564
+
565
+ this.logger.info("Initial task message completed", {
566
+ stopReason: result.stopReason,
567
+ });
568
+
569
+ // Only auto-complete for background mode
570
+ const mode = this.getEffectiveMode(payload);
571
+ if (mode === "background") {
572
+ await this.signalTaskComplete(payload, result.stopReason);
573
+ } else {
574
+ this.logger.info("Interactive mode - staying open for conversation");
575
+ }
576
+ } catch (error) {
577
+ this.logger.error("Failed to send initial task message", error);
578
+ // Signal failure for background mode
579
+ const mode = this.getEffectiveMode(payload);
580
+ if (mode === "background") {
581
+ await this.signalTaskComplete(payload, "error");
582
+ }
583
+ }
584
+ }
585
+
586
+ private async signalTaskComplete(
587
+ payload: JwtPayload,
588
+ stopReason: string,
589
+ ): Promise<void> {
590
+ const status =
591
+ stopReason === "cancelled"
592
+ ? "cancelled"
593
+ : stopReason === "error"
594
+ ? "failed"
595
+ : "completed";
596
+
597
+ try {
598
+ await this.posthogAPI.updateTaskRun(payload.task_id, payload.run_id, {
599
+ status,
600
+ error_message: stopReason === "error" ? "Agent error" : undefined,
601
+ });
602
+ this.logger.info("Task completion signaled", { status, stopReason });
603
+ } catch (error) {
604
+ this.logger.error("Failed to signal task completion", error);
605
+ }
606
+ }
607
+
608
+ private configureEnvironment(): void {
609
+ const { apiKey, apiUrl, projectId } = this.config;
610
+ const gatewayUrl = process.env.LLM_GATEWAY_URL || getLlmGatewayUrl(apiUrl);
611
+ const openaiBaseUrl = gatewayUrl.endsWith("/v1")
612
+ ? gatewayUrl
613
+ : `${gatewayUrl}/v1`;
614
+
615
+ Object.assign(process.env, {
616
+ // PostHog
617
+ POSTHOG_API_KEY: apiKey,
618
+ POSTHOG_API_URL: apiUrl,
619
+ POSTHOG_API_HOST: apiUrl,
620
+ POSTHOG_AUTH_HEADER: `Bearer ${apiKey}`,
621
+ POSTHOG_PROJECT_ID: String(projectId),
622
+ // Anthropic
623
+ ANTHROPIC_API_KEY: apiKey,
624
+ ANTHROPIC_AUTH_TOKEN: apiKey,
625
+ ANTHROPIC_BASE_URL: gatewayUrl,
626
+ // OpenAI (for models like GPT-4, o1, etc.)
627
+ OPENAI_API_KEY: apiKey,
628
+ OPENAI_BASE_URL: openaiBaseUrl,
629
+ // Generic gateway
630
+ LLM_GATEWAY_URL: gatewayUrl,
631
+ });
632
+ }
633
+
634
+ private createCloudClient(payload: JwtPayload) {
635
+ const mode = this.getEffectiveMode(payload);
636
+
637
+ return {
638
+ requestPermission: async (params: {
639
+ options: Array<{ kind: string; optionId: string }>;
640
+ }) => {
641
+ // Background mode: always auto-approve permissions
642
+ // Interactive mode: also auto-approve for now (user can monitor via SSE)
643
+ // Future: interactive mode could pause and wait for user approval via SSE
644
+ this.logger.debug("Permission request", {
645
+ mode,
646
+ options: params.options,
647
+ });
648
+
649
+ const allowOption = params.options.find(
650
+ (o) => o.kind === "allow_once" || o.kind === "allow_always",
651
+ );
652
+ return {
653
+ outcome: {
654
+ outcome: "selected" as const,
655
+ optionId: allowOption?.optionId ?? params.options[0].optionId,
656
+ },
657
+ };
658
+ },
659
+ sessionUpdate: async (params: {
660
+ sessionId: string;
661
+ update?: Record<string, unknown>;
662
+ }) => {
663
+ // session/update notifications flow through the tapped stream (like local transport)
664
+ // Only handle tree state capture for file changes here
665
+ if (params.update?.sessionUpdate === "tool_call_update") {
666
+ const meta = (params.update?._meta as Record<string, unknown>)
667
+ ?.claudeCode as Record<string, unknown> | undefined;
668
+ const toolName = meta?.toolName as string | undefined;
669
+ const toolResponse = meta?.toolResponse as
670
+ | Record<string, unknown>
671
+ | undefined;
672
+
673
+ if (
674
+ (toolName === "Write" || toolName === "Edit") &&
675
+ toolResponse?.filePath
676
+ ) {
677
+ await this.captureTreeState();
678
+ }
679
+ }
680
+ },
681
+ };
682
+ }
683
+
684
+ private async cleanupSession(): Promise<void> {
685
+ if (!this.session) return;
686
+
687
+ this.logger.info("Cleaning up session");
688
+
689
+ try {
690
+ await this.captureTreeState();
691
+ } catch (error) {
692
+ this.logger.error("Failed to capture final tree state", error);
693
+ }
694
+
695
+ try {
696
+ await this.session.logWriter.flush(this.session.payload.run_id);
697
+ } catch (error) {
698
+ this.logger.error("Failed to flush session logs", error);
699
+ }
700
+
701
+ try {
702
+ await this.session.acpConnection.cleanup();
703
+ } catch (error) {
704
+ this.logger.error("Failed to cleanup ACP connection", error);
705
+ }
706
+
707
+ if (this.session.sseController) {
708
+ this.session.sseController.close();
709
+ }
710
+
711
+ this.session = null;
712
+ }
713
+
714
+ private async captureTreeState(): Promise<void> {
715
+ if (!this.session?.treeTracker) return;
716
+
717
+ try {
718
+ const snapshot = await this.session.treeTracker.captureTree({});
719
+ if (snapshot) {
720
+ const snapshotWithDevice: TreeSnapshotEvent = {
721
+ ...snapshot,
722
+ device: this.session.deviceInfo,
723
+ };
724
+ this.broadcastEvent({
725
+ type: "notification",
726
+ timestamp: new Date().toISOString(),
727
+ notification: {
728
+ jsonrpc: "2.0",
729
+ method: POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT,
730
+ params: snapshotWithDevice,
731
+ },
732
+ });
733
+ }
734
+ } catch (error) {
735
+ this.logger.error("Failed to capture tree state", error);
736
+ }
737
+ }
738
+
739
+ private broadcastEvent(event: Record<string, unknown>): void {
740
+ if (this.session?.sseController) {
741
+ this.sendSseEvent(this.session.sseController, event);
742
+ }
743
+ }
744
+
745
+ private sendSseEvent(controller: SseController, data: unknown): void {
746
+ controller.send(data);
747
+ }
748
+ }