@posthog/agent 1.30.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 (144) 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 +172 -1203
  35. package/dist/index.js +3704 -6826
  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 +93 -61
  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 -611
  70. package/src/adapters/claude/types.ts +61 -0
  71. package/src/adapters/codex/spawn.ts +130 -0
  72. package/src/agent.ts +97 -734
  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 +51 -154
  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/dist/templates/plan-template.md +0 -41
  119. package/src/adapters/claude/claude.ts +0 -1543
  120. package/src/adapters/claude/mcp-server.ts +0 -810
  121. package/src/adapters/claude/utils.ts +0 -267
  122. package/src/agents/execution.ts +0 -37
  123. package/src/agents/planning.ts +0 -60
  124. package/src/agents/research.ts +0 -160
  125. package/src/file-manager.ts +0 -306
  126. package/src/git-manager.ts +0 -577
  127. package/src/prompt-builder.ts +0 -499
  128. package/src/schemas.ts +0 -241
  129. package/src/session-store.ts +0 -259
  130. package/src/task-manager.ts +0 -163
  131. package/src/template-manager.ts +0 -236
  132. package/src/templates/plan-template.md +0 -41
  133. package/src/todo-manager.ts +0 -180
  134. package/src/tools/registry.ts +0 -129
  135. package/src/tools/types.ts +0 -127
  136. package/src/utils/tapped-stream.ts +0 -60
  137. package/src/workflow/config.ts +0 -53
  138. package/src/workflow/steps/build.ts +0 -135
  139. package/src/workflow/steps/finalize.ts +0 -241
  140. package/src/workflow/steps/plan.ts +0 -167
  141. package/src/workflow/steps/research.ts +0 -223
  142. package/src/workflow/types.ts +0 -62
  143. package/src/workflow/utils.ts +0 -53
  144. package/src/worktree-manager.ts +0 -928
@@ -0,0 +1,4507 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server/bin.ts
4
+ import { Command } from "commander";
5
+ import { z as z4 } from "zod";
6
+
7
+ // src/server/agent-server.ts
8
+ import {
9
+ ClientSideConnection,
10
+ ndJsonStream as ndJsonStream2,
11
+ PROTOCOL_VERSION
12
+ } from "@agentclientprotocol/sdk";
13
+ import { serve } from "@hono/node-server";
14
+ import { Hono } from "hono";
15
+
16
+ // src/acp-extensions.ts
17
+ var POSTHOG_NOTIFICATIONS = {
18
+ /** Git branch was created for a task */
19
+ BRANCH_CREATED: "_posthog/branch_created",
20
+ /** Task run has started execution */
21
+ RUN_STARTED: "_posthog/run_started",
22
+ /** Task has completed (success or failure) */
23
+ TASK_COMPLETE: "_posthog/task_complete",
24
+ /** Error occurred during task execution */
25
+ ERROR: "_posthog/error",
26
+ /** Console/log output from the agent */
27
+ CONSOLE: "_posthog/console",
28
+ /** Maps taskRunId to agent's sessionId and adapter type (for resumption) */
29
+ SDK_SESSION: "_posthog/sdk_session",
30
+ /** Tree state snapshot captured (git tree hash + file archive) */
31
+ TREE_SNAPSHOT: "_posthog/tree_snapshot",
32
+ /** Agent mode changed (interactive/background) */
33
+ MODE_CHANGE: "_posthog/mode_change",
34
+ /** Request to resume a session from previous state */
35
+ SESSION_RESUME: "_posthog/session/resume",
36
+ /** User message sent from client to agent */
37
+ USER_MESSAGE: "_posthog/user_message",
38
+ /** Request to cancel current operation */
39
+ CANCEL: "_posthog/cancel",
40
+ /** Request to close the session */
41
+ CLOSE: "_posthog/close",
42
+ /** Agent status update (thinking, working, etc.) */
43
+ STATUS: "_posthog/status",
44
+ /** Task-level notification (progress, milestones) */
45
+ TASK_NOTIFICATION: "_posthog/task_notification",
46
+ /** Marks a boundary for log compaction */
47
+ COMPACT_BOUNDARY: "_posthog/compact_boundary"
48
+ };
49
+
50
+ // src/adapters/acp-connection.ts
51
+ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
52
+
53
+ // src/utils/logger.ts
54
+ var Logger = class _Logger {
55
+ debugEnabled;
56
+ prefix;
57
+ scope;
58
+ onLog;
59
+ constructor(config = {}) {
60
+ this.debugEnabled = config.debug ?? false;
61
+ this.prefix = config.prefix ?? "[PostHog Agent]";
62
+ this.scope = config.scope ?? "agent";
63
+ this.onLog = config.onLog;
64
+ }
65
+ formatMessage(level, message, data) {
66
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
67
+ const base = `${timestamp} ${this.prefix} [${level}] ${message}`;
68
+ if (data !== void 0) {
69
+ return `${base} ${JSON.stringify(data, null, 2)}`;
70
+ }
71
+ return base;
72
+ }
73
+ emitLog(level, message, data) {
74
+ if (this.onLog) {
75
+ this.onLog(level, this.scope, message, data);
76
+ return;
77
+ }
78
+ const shouldLog = this.debugEnabled || level === "error";
79
+ if (shouldLog) {
80
+ console[level](this.formatMessage(level.toLowerCase(), message, data));
81
+ }
82
+ }
83
+ error(message, error) {
84
+ const data = error instanceof Error ? { message: error.message, stack: error.stack } : error;
85
+ this.emitLog("error", message, data);
86
+ }
87
+ warn(message, data) {
88
+ this.emitLog("warn", message, data);
89
+ }
90
+ info(message, data) {
91
+ this.emitLog("info", message, data);
92
+ }
93
+ debug(message, data) {
94
+ this.emitLog("debug", message, data);
95
+ }
96
+ child(childPrefix) {
97
+ return new _Logger({
98
+ debug: this.debugEnabled,
99
+ prefix: `${this.prefix} [${childPrefix}]`,
100
+ scope: `${this.scope}:${childPrefix}`,
101
+ onLog: this.onLog
102
+ });
103
+ }
104
+ };
105
+
106
+ // src/utils/streams.ts
107
+ import { ReadableStream as ReadableStream2, WritableStream as WritableStream2 } from "stream/web";
108
+ var Pushable = class {
109
+ queue = [];
110
+ resolvers = [];
111
+ done = false;
112
+ push(item) {
113
+ const resolve3 = this.resolvers.shift();
114
+ if (resolve3) {
115
+ resolve3({ value: item, done: false });
116
+ } else {
117
+ this.queue.push(item);
118
+ }
119
+ }
120
+ end() {
121
+ this.done = true;
122
+ for (const resolve3 of this.resolvers) {
123
+ resolve3({ value: void 0, done: true });
124
+ }
125
+ this.resolvers = [];
126
+ }
127
+ [Symbol.asyncIterator]() {
128
+ return {
129
+ next: () => {
130
+ if (this.queue.length > 0) {
131
+ const value = this.queue.shift();
132
+ return Promise.resolve({ value, done: false });
133
+ }
134
+ if (this.done) {
135
+ return Promise.resolve({
136
+ value: void 0,
137
+ done: true
138
+ });
139
+ }
140
+ return new Promise((resolve3) => {
141
+ this.resolvers.push(resolve3);
142
+ });
143
+ }
144
+ };
145
+ }
146
+ };
147
+ function pushableToReadableStream(pushable) {
148
+ const iterator = pushable[Symbol.asyncIterator]();
149
+ return new ReadableStream2({
150
+ async pull(controller) {
151
+ const { value, done } = await iterator.next();
152
+ if (done) {
153
+ controller.close();
154
+ } else {
155
+ controller.enqueue(value);
156
+ }
157
+ }
158
+ });
159
+ }
160
+ function createBidirectionalStreams() {
161
+ const clientToAgentPushable = new Pushable();
162
+ const agentToClientPushable = new Pushable();
163
+ const clientToAgentReadable = pushableToReadableStream(clientToAgentPushable);
164
+ const agentToClientReadable = pushableToReadableStream(agentToClientPushable);
165
+ const clientToAgentWritable = new WritableStream2({
166
+ write(chunk) {
167
+ clientToAgentPushable.push(chunk);
168
+ },
169
+ close() {
170
+ clientToAgentPushable.end();
171
+ }
172
+ });
173
+ const agentToClientWritable = new WritableStream2({
174
+ write(chunk) {
175
+ agentToClientPushable.push(chunk);
176
+ },
177
+ close() {
178
+ agentToClientPushable.end();
179
+ }
180
+ });
181
+ return {
182
+ client: {
183
+ readable: agentToClientReadable,
184
+ writable: clientToAgentWritable
185
+ },
186
+ agent: {
187
+ readable: clientToAgentReadable,
188
+ writable: agentToClientWritable
189
+ }
190
+ };
191
+ }
192
+ function createTappedWritableStream(underlying, options) {
193
+ const { onMessage, logger } = options;
194
+ const decoder = new TextDecoder();
195
+ let buffer = "";
196
+ let _messageCount = 0;
197
+ return new WritableStream2({
198
+ async write(chunk) {
199
+ buffer += decoder.decode(chunk, { stream: true });
200
+ const lines = buffer.split("\n");
201
+ buffer = lines.pop() ?? "";
202
+ for (const line of lines) {
203
+ if (!line.trim()) continue;
204
+ _messageCount++;
205
+ onMessage(line);
206
+ }
207
+ try {
208
+ const writer = underlying.getWriter();
209
+ await writer.write(chunk);
210
+ writer.releaseLock();
211
+ } catch (err) {
212
+ logger?.error("ACP write error", err);
213
+ }
214
+ },
215
+ async close() {
216
+ try {
217
+ const writer = underlying.getWriter();
218
+ await writer.close();
219
+ writer.releaseLock();
220
+ } catch {
221
+ }
222
+ },
223
+ async abort(reason) {
224
+ logger?.warn("Tapped stream aborted", { reason });
225
+ try {
226
+ const writer = underlying.getWriter();
227
+ await writer.abort(reason);
228
+ writer.releaseLock();
229
+ } catch {
230
+ }
231
+ }
232
+ });
233
+ }
234
+ function nodeReadableToWebReadable(nodeStream) {
235
+ return new ReadableStream2({
236
+ start(controller) {
237
+ nodeStream.on("data", (chunk) => {
238
+ controller.enqueue(new Uint8Array(chunk));
239
+ });
240
+ nodeStream.on("end", () => {
241
+ controller.close();
242
+ });
243
+ nodeStream.on("error", (err) => {
244
+ controller.error(err);
245
+ });
246
+ },
247
+ cancel() {
248
+ nodeStream.destroy();
249
+ }
250
+ });
251
+ }
252
+ function nodeWritableToWebWritable(nodeStream) {
253
+ return new WritableStream2({
254
+ write(chunk) {
255
+ return new Promise((resolve3, reject) => {
256
+ const ok = nodeStream.write(Buffer.from(chunk), (err) => {
257
+ if (err) reject(err);
258
+ });
259
+ if (ok) {
260
+ resolve3();
261
+ } else {
262
+ nodeStream.once("drain", resolve3);
263
+ }
264
+ });
265
+ },
266
+ close() {
267
+ return new Promise((resolve3) => {
268
+ nodeStream.end(resolve3);
269
+ });
270
+ },
271
+ abort(reason) {
272
+ nodeStream.destroy(
273
+ reason instanceof Error ? reason : new Error(String(reason))
274
+ );
275
+ }
276
+ });
277
+ }
278
+
279
+ // src/adapters/claude/claude-agent.ts
280
+ import * as fs2 from "fs";
281
+ import * as os3 from "os";
282
+ import * as path3 from "path";
283
+ import {
284
+ RequestError as RequestError2
285
+ } from "@agentclientprotocol/sdk";
286
+ import {
287
+ query
288
+ } from "@anthropic-ai/claude-agent-sdk";
289
+ import { v7 as uuidv7 } from "uuid";
290
+
291
+ // package.json
292
+ var package_default = {
293
+ name: "@posthog/agent",
294
+ version: "2.0.1",
295
+ repository: "https://github.com/PostHog/twig",
296
+ description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
297
+ exports: {
298
+ ".": {
299
+ types: "./dist/index.d.ts",
300
+ import: "./dist/index.js"
301
+ },
302
+ "./agent": {
303
+ types: "./dist/agent.d.ts",
304
+ import: "./dist/agent.js"
305
+ },
306
+ "./gateway-models": {
307
+ types: "./dist/gateway-models.d.ts",
308
+ import: "./dist/gateway-models.js"
309
+ },
310
+ "./posthog-api": {
311
+ types: "./dist/posthog-api.d.ts",
312
+ import: "./dist/posthog-api.js"
313
+ },
314
+ "./types": {
315
+ types: "./dist/types.d.ts",
316
+ import: "./dist/types.js"
317
+ },
318
+ "./adapters/claude/questions/utils": {
319
+ types: "./dist/adapters/claude/questions/utils.d.ts",
320
+ import: "./dist/adapters/claude/questions/utils.js"
321
+ },
322
+ "./adapters/claude/permissions/permission-options": {
323
+ types: "./dist/adapters/claude/permissions/permission-options.d.ts",
324
+ import: "./dist/adapters/claude/permissions/permission-options.js"
325
+ },
326
+ "./adapters/claude/tools": {
327
+ types: "./dist/adapters/claude/tools.d.ts",
328
+ import: "./dist/adapters/claude/tools.js"
329
+ },
330
+ "./adapters/claude/conversion/tool-use-to-acp": {
331
+ types: "./dist/adapters/claude/conversion/tool-use-to-acp.d.ts",
332
+ import: "./dist/adapters/claude/conversion/tool-use-to-acp.js"
333
+ },
334
+ "./server": {
335
+ types: "./dist/server/agent-server.d.ts",
336
+ import: "./dist/server/agent-server.js"
337
+ }
338
+ },
339
+ bin: {
340
+ "agent-server": "./dist/server/bin.js"
341
+ },
342
+ type: "module",
343
+ keywords: [
344
+ "posthog",
345
+ "claude",
346
+ "agent",
347
+ "ai",
348
+ "git",
349
+ "typescript"
350
+ ],
351
+ author: "PostHog",
352
+ license: "SEE LICENSE IN LICENSE",
353
+ scripts: {
354
+ build: "tsup",
355
+ dev: "tsup --watch",
356
+ test: "vitest run",
357
+ "test:watch": "vitest",
358
+ typecheck: "pnpm exec tsc --noEmit",
359
+ prepublishOnly: "pnpm run build"
360
+ },
361
+ engines: {
362
+ node: ">=20.0.0"
363
+ },
364
+ devDependencies: {
365
+ "@changesets/cli": "^2.27.8",
366
+ "@types/bun": "latest",
367
+ "@types/tar": "^6.1.13",
368
+ minimatch: "^10.0.3",
369
+ msw: "^2.12.7",
370
+ tsup: "^8.5.1",
371
+ tsx: "^4.20.6",
372
+ typescript: "^5.5.0",
373
+ vitest: "^2.1.8"
374
+ },
375
+ dependencies: {
376
+ "@opentelemetry/api-logs": "^0.208.0",
377
+ "@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
378
+ "@opentelemetry/resources": "^2.0.0",
379
+ "@opentelemetry/sdk-logs": "^0.208.0",
380
+ "@opentelemetry/semantic-conventions": "^1.28.0",
381
+ "@agentclientprotocol/sdk": "^0.14.0",
382
+ "@anthropic-ai/claude-agent-sdk": "0.2.12",
383
+ "@anthropic-ai/sdk": "^0.71.0",
384
+ "@hono/node-server": "^1.19.9",
385
+ "@modelcontextprotocol/sdk": "^1.25.3",
386
+ "@posthog/shared": "workspace:*",
387
+ "@twig/git": "workspace:*",
388
+ "@types/jsonwebtoken": "^9.0.10",
389
+ commander: "^14.0.2",
390
+ diff: "^8.0.2",
391
+ dotenv: "^17.2.3",
392
+ hono: "^4.11.7",
393
+ jsonwebtoken: "^9.0.2",
394
+ tar: "^7.5.0",
395
+ uuid: "13.0.0",
396
+ "yoga-wasm-web": "^0.3.3",
397
+ zod: "^3.24.1"
398
+ },
399
+ files: [
400
+ "dist/**/*",
401
+ "src/**/*",
402
+ "README.md",
403
+ "CLAUDE.md"
404
+ ],
405
+ publishConfig: {
406
+ access: "public"
407
+ }
408
+ };
409
+
410
+ // src/utils/common.ts
411
+ var IS_ROOT = typeof process !== "undefined" && (process.geteuid?.() ?? process.getuid?.()) === 0;
412
+ function unreachable(value, logger) {
413
+ let valueAsString;
414
+ try {
415
+ valueAsString = JSON.stringify(value);
416
+ } catch {
417
+ valueAsString = value;
418
+ }
419
+ logger.error(`Unexpected case: ${valueAsString}`);
420
+ }
421
+
422
+ // src/gateway-models.ts
423
+ var DEFAULT_GATEWAY_MODEL = "claude-opus-4-6";
424
+ var BLOCKED_MODELS = /* @__PURE__ */ new Set(["gpt-5-mini", "openai/gpt-5-mini"]);
425
+ async function fetchGatewayModels(options) {
426
+ const gatewayUrl = options?.gatewayUrl ?? process.env.ANTHROPIC_BASE_URL;
427
+ if (!gatewayUrl) {
428
+ return [];
429
+ }
430
+ const modelsUrl = `${gatewayUrl}/v1/models`;
431
+ try {
432
+ const response = await fetch(modelsUrl);
433
+ if (!response.ok) {
434
+ return [];
435
+ }
436
+ const data = await response.json();
437
+ const models = data.data ?? [];
438
+ return models.filter((m) => !BLOCKED_MODELS.has(m.id));
439
+ } catch {
440
+ return [];
441
+ }
442
+ }
443
+ function isAnthropicModel(model) {
444
+ if (model.owned_by) {
445
+ return model.owned_by === "anthropic";
446
+ }
447
+ return model.id.startsWith("claude-") || model.id.startsWith("anthropic/");
448
+ }
449
+ var PROVIDER_PREFIXES = ["anthropic/", "openai/", "google-vertex/"];
450
+ function formatGatewayModelName(model) {
451
+ let cleanId = model.id;
452
+ for (const prefix of PROVIDER_PREFIXES) {
453
+ if (cleanId.startsWith(prefix)) {
454
+ cleanId = cleanId.slice(prefix.length);
455
+ break;
456
+ }
457
+ }
458
+ cleanId = cleanId.replace(/(\d)-(\d)/g, "$1.$2");
459
+ const words = cleanId.split(/[-_]/).map((word) => {
460
+ if (word.match(/^[0-9.]+$/)) return word;
461
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
462
+ });
463
+ return words.join(" ");
464
+ }
465
+
466
+ // src/adapters/base-acp-agent.ts
467
+ var BaseAcpAgent = class {
468
+ session;
469
+ sessionId;
470
+ client;
471
+ logger;
472
+ fileContentCache = {};
473
+ constructor(client) {
474
+ this.client = client;
475
+ this.logger = new Logger({ debug: true, prefix: "[BaseAcpAgent]" });
476
+ }
477
+ async cancel(params) {
478
+ if (this.sessionId !== params.sessionId) {
479
+ throw new Error("Session not found");
480
+ }
481
+ this.session.cancelled = true;
482
+ const meta = params._meta;
483
+ if (meta?.interruptReason) {
484
+ this.session.interruptReason = meta.interruptReason;
485
+ }
486
+ await this.interruptSession();
487
+ }
488
+ async closeSession() {
489
+ try {
490
+ this.session.abortController.abort();
491
+ await this.cancel({ sessionId: this.sessionId });
492
+ this.logger.info("Closed session", { sessionId: this.sessionId });
493
+ } catch (err) {
494
+ this.logger.warn("Failed to close session", {
495
+ sessionId: this.sessionId,
496
+ error: err
497
+ });
498
+ }
499
+ }
500
+ hasSession(sessionId) {
501
+ return this.sessionId === sessionId;
502
+ }
503
+ appendNotification(sessionId, notification) {
504
+ if (this.sessionId === sessionId) {
505
+ this.session.notificationHistory.push(notification);
506
+ }
507
+ }
508
+ async readTextFile(params) {
509
+ const response = await this.client.readTextFile(params);
510
+ if (!params.limit && !params.line) {
511
+ this.fileContentCache[params.path] = response.content;
512
+ }
513
+ return response;
514
+ }
515
+ async writeTextFile(params) {
516
+ const response = await this.client.writeTextFile(params);
517
+ this.fileContentCache[params.path] = params.content;
518
+ return response;
519
+ }
520
+ async authenticate(_params) {
521
+ throw new Error("Method not implemented.");
522
+ }
523
+ async getModelConfigOptions(currentModelOverride) {
524
+ const gatewayModels = await fetchGatewayModels();
525
+ const options = gatewayModels.filter((model) => isAnthropicModel(model)).map((model) => ({
526
+ value: model.id,
527
+ name: formatGatewayModelName(model),
528
+ description: `Context: ${model.context_window.toLocaleString()} tokens`
529
+ }));
530
+ const isAnthropicModelId = (modelId) => modelId.startsWith("claude-") || modelId.startsWith("anthropic/");
531
+ let currentModelId = currentModelOverride ?? DEFAULT_GATEWAY_MODEL;
532
+ if (!options.some((opt) => opt.value === currentModelId)) {
533
+ if (!isAnthropicModelId(currentModelId)) {
534
+ currentModelId = DEFAULT_GATEWAY_MODEL;
535
+ }
536
+ }
537
+ if (!options.some((opt) => opt.value === currentModelId)) {
538
+ options.unshift({
539
+ value: currentModelId,
540
+ name: currentModelId,
541
+ description: "Custom model"
542
+ });
543
+ }
544
+ return { currentModelId, options };
545
+ }
546
+ };
547
+
548
+ // src/adapters/claude/conversion/acp-to-sdk.ts
549
+ function sdkText(value) {
550
+ return { type: "text", text: value };
551
+ }
552
+ function formatUriAsLink(uri) {
553
+ try {
554
+ if (uri.startsWith("file://")) {
555
+ const filePath = uri.slice(7);
556
+ const name = filePath.split("/").pop() || filePath;
557
+ return `[@${name}](${uri})`;
558
+ }
559
+ if (uri.startsWith("zed://")) {
560
+ const parts = uri.split("/");
561
+ const name = parts[parts.length - 1] || uri;
562
+ return `[@${name}](${uri})`;
563
+ }
564
+ return uri;
565
+ } catch {
566
+ return uri;
567
+ }
568
+ }
569
+ function transformMcpCommand(text2) {
570
+ const mcpMatch = text2.match(/^\/mcp:([^:\s]+):(\S+)(\s+.*)?$/);
571
+ if (mcpMatch) {
572
+ const [, server, command, args] = mcpMatch;
573
+ return `/${server}:${command} (MCP)${args || ""}`;
574
+ }
575
+ return text2;
576
+ }
577
+ function processPromptChunk(chunk, content, context) {
578
+ switch (chunk.type) {
579
+ case "text":
580
+ content.push(sdkText(transformMcpCommand(chunk.text)));
581
+ break;
582
+ case "resource_link":
583
+ content.push(sdkText(formatUriAsLink(chunk.uri)));
584
+ break;
585
+ case "resource":
586
+ if ("text" in chunk.resource) {
587
+ content.push(sdkText(formatUriAsLink(chunk.resource.uri)));
588
+ context.push(
589
+ sdkText(
590
+ `
591
+ <context ref="${chunk.resource.uri}">
592
+ ${chunk.resource.text}
593
+ </context>`
594
+ )
595
+ );
596
+ }
597
+ break;
598
+ case "image":
599
+ if (chunk.data) {
600
+ content.push({
601
+ type: "image",
602
+ source: {
603
+ type: "base64",
604
+ data: chunk.data,
605
+ media_type: chunk.mimeType
606
+ }
607
+ });
608
+ } else if (chunk.uri?.startsWith("http")) {
609
+ content.push({
610
+ type: "image",
611
+ source: { type: "url", url: chunk.uri }
612
+ });
613
+ }
614
+ break;
615
+ default:
616
+ break;
617
+ }
618
+ }
619
+ function promptToClaude(prompt) {
620
+ const content = [];
621
+ const context = [];
622
+ for (const chunk of prompt.prompt) {
623
+ processPromptChunk(chunk, content, context);
624
+ }
625
+ content.push(...context);
626
+ return {
627
+ type: "user",
628
+ message: { role: "user", content },
629
+ session_id: prompt.sessionId,
630
+ parent_tool_use_id: null
631
+ };
632
+ }
633
+
634
+ // src/adapters/claude/conversion/sdk-to-acp.ts
635
+ import { RequestError } from "@agentclientprotocol/sdk";
636
+
637
+ // src/utils/acp-content.ts
638
+ function text(value) {
639
+ return { type: "text", text: value };
640
+ }
641
+ function image(data, mimeType, uri) {
642
+ return { type: "image", data, mimeType, uri };
643
+ }
644
+ function resourceLink(uri, name, options) {
645
+ return {
646
+ type: "resource_link",
647
+ uri,
648
+ name,
649
+ ...options
650
+ };
651
+ }
652
+ var ToolContentBuilder = class {
653
+ items = [];
654
+ text(value) {
655
+ this.items.push({ type: "content", content: text(value) });
656
+ return this;
657
+ }
658
+ image(data, mimeType, uri) {
659
+ this.items.push({ type: "content", content: image(data, mimeType, uri) });
660
+ return this;
661
+ }
662
+ diff(path4, oldText, newText) {
663
+ this.items.push({ type: "diff", path: path4, oldText, newText });
664
+ return this;
665
+ }
666
+ build() {
667
+ return this.items;
668
+ }
669
+ };
670
+ function toolContent() {
671
+ return new ToolContentBuilder();
672
+ }
673
+
674
+ // src/adapters/claude/hooks.ts
675
+ var toolUseCallbacks = {};
676
+ var registerHookCallback = (toolUseID, {
677
+ onPostToolUseHook
678
+ }) => {
679
+ toolUseCallbacks[toolUseID] = {
680
+ onPostToolUseHook
681
+ };
682
+ };
683
+ var createPostToolUseHook = ({ onModeChange }) => async (input, toolUseID) => {
684
+ if (input.hook_event_name === "PostToolUse") {
685
+ const toolName = input.tool_name;
686
+ if (onModeChange && toolName === "EnterPlanMode") {
687
+ await onModeChange("plan");
688
+ }
689
+ if (toolUseID) {
690
+ const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook;
691
+ if (onPostToolUseHook) {
692
+ await onPostToolUseHook(
693
+ toolUseID,
694
+ input.tool_input,
695
+ input.tool_response
696
+ );
697
+ delete toolUseCallbacks[toolUseID];
698
+ }
699
+ }
700
+ }
701
+ return { continue: true };
702
+ };
703
+
704
+ // src/adapters/claude/conversion/tool-use-to-acp.ts
705
+ var SYSTEM_REMINDER = `
706
+
707
+ <system-reminder>
708
+ Whenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior.
709
+ </system-reminder>`;
710
+ function replaceAndCalculateLocation(fileContent, edits) {
711
+ let currentContent = fileContent;
712
+ const randomHex = Array.from(crypto.getRandomValues(new Uint8Array(5))).map((b) => b.toString(16).padStart(2, "0")).join("");
713
+ const markerPrefix = `__REPLACE_MARKER_${randomHex}_`;
714
+ let markerCounter = 0;
715
+ const markers = [];
716
+ for (const edit of edits) {
717
+ if (edit.oldText === "") {
718
+ throw new Error(
719
+ `The provided \`old_string\` is empty.
720
+
721
+ No edits were applied.`
722
+ );
723
+ }
724
+ if (edit.replaceAll) {
725
+ const parts = [];
726
+ let lastIndex = 0;
727
+ let searchIndex = 0;
728
+ while (true) {
729
+ const index = currentContent.indexOf(edit.oldText, searchIndex);
730
+ if (index === -1) {
731
+ if (searchIndex === 0) {
732
+ throw new Error(
733
+ `The provided \`old_string\` does not appear in the file: "${edit.oldText}".
734
+
735
+ No edits were applied.`
736
+ );
737
+ }
738
+ break;
739
+ }
740
+ parts.push(currentContent.substring(lastIndex, index));
741
+ const marker = `${markerPrefix}${markerCounter++}__`;
742
+ markers.push(marker);
743
+ parts.push(marker + edit.newText);
744
+ lastIndex = index + edit.oldText.length;
745
+ searchIndex = lastIndex;
746
+ }
747
+ parts.push(currentContent.substring(lastIndex));
748
+ currentContent = parts.join("");
749
+ } else {
750
+ const index = currentContent.indexOf(edit.oldText);
751
+ if (index === -1) {
752
+ throw new Error(
753
+ `The provided \`old_string\` does not appear in the file: "${edit.oldText}".
754
+
755
+ No edits were applied.`
756
+ );
757
+ } else {
758
+ const marker = `${markerPrefix}${markerCounter++}__`;
759
+ markers.push(marker);
760
+ currentContent = currentContent.substring(0, index) + marker + edit.newText + currentContent.substring(index + edit.oldText.length);
761
+ }
762
+ }
763
+ }
764
+ const lineNumbers = [];
765
+ for (const marker of markers) {
766
+ const index = currentContent.indexOf(marker);
767
+ if (index !== -1) {
768
+ const lineNumber = Math.max(
769
+ 0,
770
+ currentContent.substring(0, index).split(/\r\n|\r|\n/).length - 1
771
+ );
772
+ lineNumbers.push(lineNumber);
773
+ }
774
+ }
775
+ let finalContent = currentContent;
776
+ for (const marker of markers) {
777
+ finalContent = finalContent.replace(marker, "");
778
+ }
779
+ const uniqueLineNumbers = [...new Set(lineNumbers)].sort();
780
+ return { newContent: finalContent, lineNumbers: uniqueLineNumbers };
781
+ }
782
+ function toolInfoFromToolUse(toolUse, cachedFileContent, logger = new Logger({ debug: false, prefix: "[ClaudeTools]" })) {
783
+ const name = toolUse.name;
784
+ const input = toolUse.input;
785
+ switch (name) {
786
+ case "Task":
787
+ return {
788
+ title: input?.description ? String(input.description) : "Task",
789
+ kind: "think",
790
+ content: input?.prompt ? toolContent().text(String(input.prompt)).build() : []
791
+ };
792
+ case "NotebookRead":
793
+ return {
794
+ title: input?.notebook_path ? `Read Notebook ${String(input.notebook_path)}` : "Read Notebook",
795
+ kind: "read",
796
+ content: [],
797
+ locations: input?.notebook_path ? [{ path: String(input.notebook_path) }] : []
798
+ };
799
+ case "NotebookEdit":
800
+ return {
801
+ title: input?.notebook_path ? `Edit Notebook ${String(input.notebook_path)}` : "Edit Notebook",
802
+ kind: "edit",
803
+ content: input?.new_source ? toolContent().text(String(input.new_source)).build() : [],
804
+ locations: input?.notebook_path ? [{ path: String(input.notebook_path) }] : []
805
+ };
806
+ case "Bash":
807
+ return {
808
+ title: input?.description ? String(input.description) : "Execute command",
809
+ kind: "execute",
810
+ content: input?.command ? toolContent().text(String(input.command)).build() : []
811
+ };
812
+ case "BashOutput":
813
+ return {
814
+ title: "Tail Logs",
815
+ kind: "execute",
816
+ content: []
817
+ };
818
+ case "KillShell":
819
+ return {
820
+ title: "Kill Process",
821
+ kind: "execute",
822
+ content: []
823
+ };
824
+ case "Read": {
825
+ let limit = "";
826
+ const inputLimit = input?.limit;
827
+ const inputOffset = input?.offset ?? 0;
828
+ if (inputLimit) {
829
+ limit = ` (${inputOffset + 1} - ${inputOffset + inputLimit})`;
830
+ } else if (inputOffset) {
831
+ limit = ` (from line ${inputOffset + 1})`;
832
+ }
833
+ return {
834
+ title: `Read ${input?.file_path ? String(input.file_path) : "File"}${limit}`,
835
+ kind: "read",
836
+ locations: input?.file_path ? [
837
+ {
838
+ path: String(input.file_path),
839
+ line: inputOffset
840
+ }
841
+ ] : [],
842
+ content: []
843
+ };
844
+ }
845
+ case "LS":
846
+ return {
847
+ title: `List the ${input?.path ? `\`${String(input.path)}\`` : "current"} directory's contents`,
848
+ kind: "search",
849
+ content: [],
850
+ locations: []
851
+ };
852
+ case "Edit": {
853
+ const path4 = input?.file_path ? String(input.file_path) : void 0;
854
+ let oldText = input?.old_string ? String(input.old_string) : null;
855
+ let newText = input?.new_string ? String(input.new_string) : "";
856
+ let affectedLines = [];
857
+ if (path4 && oldText) {
858
+ try {
859
+ const oldContent = cachedFileContent[path4] || "";
860
+ const newContent = replaceAndCalculateLocation(oldContent, [
861
+ {
862
+ oldText,
863
+ newText,
864
+ replaceAll: false
865
+ }
866
+ ]);
867
+ oldText = oldContent;
868
+ newText = newContent.newContent;
869
+ affectedLines = newContent.lineNumbers;
870
+ } catch (e) {
871
+ logger.error("Failed to edit file", e);
872
+ }
873
+ }
874
+ return {
875
+ title: path4 ? `Edit \`${path4}\`` : "Edit",
876
+ kind: "edit",
877
+ content: input && path4 ? [
878
+ {
879
+ type: "diff",
880
+ path: path4,
881
+ oldText,
882
+ newText
883
+ }
884
+ ] : [],
885
+ locations: path4 ? affectedLines.length > 0 ? affectedLines.map((line) => ({ line, path: path4 })) : [{ path: path4 }] : []
886
+ };
887
+ }
888
+ case "Write": {
889
+ let contentResult = [];
890
+ const filePath = input?.file_path ? String(input.file_path) : void 0;
891
+ const contentStr = input?.content ? String(input.content) : void 0;
892
+ if (filePath) {
893
+ contentResult = toolContent().diff(filePath, null, contentStr ?? "").build();
894
+ } else if (contentStr) {
895
+ contentResult = toolContent().text(contentStr).build();
896
+ }
897
+ return {
898
+ title: filePath ? `Write ${filePath}` : "Write",
899
+ kind: "edit",
900
+ content: contentResult,
901
+ locations: filePath ? [{ path: filePath }] : []
902
+ };
903
+ }
904
+ case "Glob": {
905
+ let label = "Find";
906
+ const pathStr = input?.path ? String(input.path) : void 0;
907
+ if (pathStr) {
908
+ label += ` "${pathStr}"`;
909
+ }
910
+ if (input?.pattern) {
911
+ label += ` "${String(input.pattern)}"`;
912
+ }
913
+ return {
914
+ title: label,
915
+ kind: "search",
916
+ content: [],
917
+ locations: pathStr ? [{ path: pathStr }] : []
918
+ };
919
+ }
920
+ case "Grep": {
921
+ let label = "grep";
922
+ if (input?.["-i"]) {
923
+ label += " -i";
924
+ }
925
+ if (input?.["-n"]) {
926
+ label += " -n";
927
+ }
928
+ if (input?.["-A"] !== void 0) {
929
+ label += ` -A ${input["-A"]}`;
930
+ }
931
+ if (input?.["-B"] !== void 0) {
932
+ label += ` -B ${input["-B"]}`;
933
+ }
934
+ if (input?.["-C"] !== void 0) {
935
+ label += ` -C ${input["-C"]}`;
936
+ }
937
+ if (input?.output_mode) {
938
+ switch (input.output_mode) {
939
+ case "FilesWithMatches":
940
+ label += " -l";
941
+ break;
942
+ case "Count":
943
+ label += " -c";
944
+ break;
945
+ default:
946
+ break;
947
+ }
948
+ }
949
+ if (input?.head_limit !== void 0) {
950
+ label += ` | head -${input.head_limit}`;
951
+ }
952
+ if (input?.glob) {
953
+ label += ` --include="${String(input.glob)}"`;
954
+ }
955
+ if (input?.type) {
956
+ label += ` --type=${String(input.type)}`;
957
+ }
958
+ if (input?.multiline) {
959
+ label += " -P";
960
+ }
961
+ label += ` "${input?.pattern ? String(input.pattern) : ""}"`;
962
+ if (input?.path) {
963
+ label += ` ${String(input.path)}`;
964
+ }
965
+ return {
966
+ title: label,
967
+ kind: "search",
968
+ content: []
969
+ };
970
+ }
971
+ case "WebFetch":
972
+ return {
973
+ title: "Fetch",
974
+ kind: "fetch",
975
+ content: input?.url ? [
976
+ {
977
+ type: "content",
978
+ content: resourceLink(String(input.url), String(input.url), {
979
+ description: input?.prompt ? String(input.prompt) : void 0
980
+ })
981
+ }
982
+ ] : []
983
+ };
984
+ case "WebSearch": {
985
+ let label = `"${input?.query ? String(input.query) : ""}"`;
986
+ const allowedDomains = input?.allowed_domains;
987
+ const blockedDomains = input?.blocked_domains;
988
+ if (allowedDomains && allowedDomains.length > 0) {
989
+ label += ` (allowed: ${allowedDomains.join(", ")})`;
990
+ }
991
+ if (blockedDomains && blockedDomains.length > 0) {
992
+ label += ` (blocked: ${blockedDomains.join(", ")})`;
993
+ }
994
+ return {
995
+ title: label,
996
+ kind: "fetch",
997
+ content: []
998
+ };
999
+ }
1000
+ case "TodoWrite":
1001
+ return {
1002
+ title: Array.isArray(input?.todos) ? `Update TODOs: ${input.todos.map((todo) => todo.content).join(", ")}` : "Update TODOs",
1003
+ kind: "think",
1004
+ content: []
1005
+ };
1006
+ case "ExitPlanMode":
1007
+ return {
1008
+ title: "Ready to code?",
1009
+ kind: "switch_mode",
1010
+ content: input?.plan ? toolContent().text(String(input.plan)).build() : []
1011
+ };
1012
+ case "AskUserQuestion": {
1013
+ const questions = input?.questions;
1014
+ return {
1015
+ title: questions?.[0]?.question || "Question",
1016
+ kind: "other",
1017
+ content: questions ? toolContent().text(JSON.stringify(questions, null, 2)).build() : []
1018
+ };
1019
+ }
1020
+ case "Other": {
1021
+ let output;
1022
+ try {
1023
+ output = JSON.stringify(input, null, 2);
1024
+ } catch {
1025
+ output = typeof input === "string" ? input : "{}";
1026
+ }
1027
+ return {
1028
+ title: name || "Unknown Tool",
1029
+ kind: "other",
1030
+ content: toolContent().text(`\`\`\`json
1031
+ ${output}\`\`\``).build()
1032
+ };
1033
+ }
1034
+ default:
1035
+ return {
1036
+ title: name || "Unknown Tool",
1037
+ kind: "other",
1038
+ content: []
1039
+ };
1040
+ }
1041
+ }
1042
+ function toolUpdateFromToolResult(toolResult, toolUse) {
1043
+ switch (toolUse?.name) {
1044
+ case "Read":
1045
+ if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
1046
+ return {
1047
+ content: toolResult.content.map((item) => {
1048
+ const itemObj = item;
1049
+ if (itemObj.type === "text") {
1050
+ return {
1051
+ type: "content",
1052
+ content: text(
1053
+ markdownEscape(
1054
+ (itemObj.text ?? "").replace(SYSTEM_REMINDER, "")
1055
+ )
1056
+ )
1057
+ };
1058
+ }
1059
+ return {
1060
+ type: "content",
1061
+ content: item
1062
+ };
1063
+ })
1064
+ };
1065
+ } else if (typeof toolResult.content === "string" && toolResult.content.length > 0) {
1066
+ return {
1067
+ content: toolContent().text(
1068
+ markdownEscape(toolResult.content.replace(SYSTEM_REMINDER, ""))
1069
+ ).build()
1070
+ };
1071
+ }
1072
+ return {};
1073
+ case "Bash": {
1074
+ return toAcpContentUpdate(
1075
+ toolResult.content,
1076
+ "is_error" in toolResult ? toolResult.is_error : false
1077
+ );
1078
+ }
1079
+ case "Edit":
1080
+ case "Write": {
1081
+ if ("is_error" in toolResult && toolResult.is_error && toolResult.content && toolResult.content.length > 0) {
1082
+ return toAcpContentUpdate(toolResult.content, true);
1083
+ }
1084
+ return {};
1085
+ }
1086
+ case "ExitPlanMode": {
1087
+ return { title: "Exited Plan Mode" };
1088
+ }
1089
+ case "AskUserQuestion": {
1090
+ const content = toolResult.content;
1091
+ if (Array.isArray(content) && content.length > 0) {
1092
+ const firstItem = content[0];
1093
+ if (typeof firstItem === "object" && firstItem !== null && "text" in firstItem) {
1094
+ return {
1095
+ title: "Answer received",
1096
+ content: toolContent().text(String(firstItem.text)).build()
1097
+ };
1098
+ }
1099
+ }
1100
+ return { title: "Question answered" };
1101
+ }
1102
+ default: {
1103
+ return toAcpContentUpdate(
1104
+ toolResult.content,
1105
+ "is_error" in toolResult ? toolResult.is_error : false
1106
+ );
1107
+ }
1108
+ }
1109
+ }
1110
+ function toAcpContentUpdate(content, isError = false) {
1111
+ if (Array.isArray(content) && content.length > 0) {
1112
+ return {
1113
+ content: content.map((item) => {
1114
+ const itemObj = item;
1115
+ if (isError && itemObj.type === "text") {
1116
+ return {
1117
+ type: "content",
1118
+ content: text(`\`\`\`
1119
+ ${itemObj.text ?? ""}
1120
+ \`\`\``)
1121
+ };
1122
+ }
1123
+ return {
1124
+ type: "content",
1125
+ content: item
1126
+ };
1127
+ })
1128
+ };
1129
+ } else if (typeof content === "string" && content.length > 0) {
1130
+ return {
1131
+ content: toolContent().text(isError ? `\`\`\`
1132
+ ${content}
1133
+ \`\`\`` : content).build()
1134
+ };
1135
+ }
1136
+ return {};
1137
+ }
1138
+ function planEntries(input) {
1139
+ return input.todos.map((input2) => ({
1140
+ content: input2.content,
1141
+ status: input2.status,
1142
+ priority: "medium"
1143
+ }));
1144
+ }
1145
+ function markdownEscape(text2) {
1146
+ let escapedText = "```";
1147
+ for (const [m] of text2.matchAll(/^```+/gm)) {
1148
+ while (m.length >= escapedText.length) {
1149
+ escapedText += "`";
1150
+ }
1151
+ }
1152
+ return `${escapedText}
1153
+ ${text2}${text2.endsWith("\n") ? "" : "\n"}${escapedText}`;
1154
+ }
1155
+
1156
+ // src/adapters/claude/conversion/sdk-to-acp.ts
1157
+ function messageUpdateType(role) {
1158
+ return role === "assistant" ? "agent_message_chunk" : "user_message_chunk";
1159
+ }
1160
+ function toolMeta(toolName, toolResponse) {
1161
+ return toolResponse ? { claudeCode: { toolName, toolResponse } } : { claudeCode: { toolName } };
1162
+ }
1163
+ function handleTextChunk(chunk, role) {
1164
+ return {
1165
+ sessionUpdate: messageUpdateType(role),
1166
+ content: text(chunk.text)
1167
+ };
1168
+ }
1169
+ function handleImageChunk(chunk, role) {
1170
+ return {
1171
+ sessionUpdate: messageUpdateType(role),
1172
+ content: image(
1173
+ chunk.source.type === "base64" ? chunk.source.data ?? "" : "",
1174
+ chunk.source.type === "base64" ? chunk.source.media_type ?? "" : "",
1175
+ chunk.source.type === "url" ? chunk.source.url : void 0
1176
+ )
1177
+ };
1178
+ }
1179
+ function handleThinkingChunk(chunk) {
1180
+ return {
1181
+ sessionUpdate: "agent_thought_chunk",
1182
+ content: text(chunk.thinking)
1183
+ };
1184
+ }
1185
+ function handleToolUseChunk(chunk, ctx) {
1186
+ ctx.toolUseCache[chunk.id] = chunk;
1187
+ if (chunk.name === "TodoWrite") {
1188
+ const input = chunk.input;
1189
+ if (Array.isArray(input.todos)) {
1190
+ return {
1191
+ sessionUpdate: "plan",
1192
+ entries: planEntries(chunk.input)
1193
+ };
1194
+ }
1195
+ return null;
1196
+ }
1197
+ registerHookCallback(chunk.id, {
1198
+ onPostToolUseHook: async (toolUseId, _toolInput, toolResponse) => {
1199
+ const toolUse = ctx.toolUseCache[toolUseId];
1200
+ if (toolUse) {
1201
+ await ctx.client.sessionUpdate({
1202
+ sessionId: ctx.sessionId,
1203
+ update: {
1204
+ _meta: toolMeta(toolUse.name, toolResponse),
1205
+ toolCallId: toolUseId,
1206
+ sessionUpdate: "tool_call_update"
1207
+ }
1208
+ });
1209
+ } else {
1210
+ ctx.logger.error(
1211
+ `Got a tool response for tool use that wasn't tracked: ${toolUseId}`
1212
+ );
1213
+ }
1214
+ }
1215
+ });
1216
+ let rawInput;
1217
+ try {
1218
+ rawInput = JSON.parse(JSON.stringify(chunk.input));
1219
+ } catch {
1220
+ }
1221
+ return {
1222
+ _meta: toolMeta(chunk.name),
1223
+ toolCallId: chunk.id,
1224
+ sessionUpdate: "tool_call",
1225
+ rawInput,
1226
+ status: "pending",
1227
+ ...toolInfoFromToolUse(chunk, ctx.fileContentCache, ctx.logger)
1228
+ };
1229
+ }
1230
+ function handleToolResultChunk(chunk, ctx) {
1231
+ const toolUse = ctx.toolUseCache[chunk.tool_use_id];
1232
+ if (!toolUse) {
1233
+ ctx.logger.error(
1234
+ `Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`
1235
+ );
1236
+ return null;
1237
+ }
1238
+ if (toolUse.name === "TodoWrite") {
1239
+ return null;
1240
+ }
1241
+ return {
1242
+ _meta: toolMeta(toolUse.name),
1243
+ toolCallId: chunk.tool_use_id,
1244
+ sessionUpdate: "tool_call_update",
1245
+ status: chunk.is_error ? "failed" : "completed",
1246
+ ...toolUpdateFromToolResult(
1247
+ chunk,
1248
+ toolUse
1249
+ )
1250
+ };
1251
+ }
1252
+ function processContentChunk(chunk, role, ctx) {
1253
+ switch (chunk.type) {
1254
+ case "text":
1255
+ case "text_delta":
1256
+ return handleTextChunk(chunk, role);
1257
+ case "image":
1258
+ return handleImageChunk(chunk, role);
1259
+ case "thinking":
1260
+ case "thinking_delta":
1261
+ return handleThinkingChunk(chunk);
1262
+ case "tool_use":
1263
+ case "server_tool_use":
1264
+ case "mcp_tool_use":
1265
+ return handleToolUseChunk(chunk, ctx);
1266
+ case "tool_result":
1267
+ case "tool_search_tool_result":
1268
+ case "web_fetch_tool_result":
1269
+ case "web_search_tool_result":
1270
+ case "code_execution_tool_result":
1271
+ case "bash_code_execution_tool_result":
1272
+ case "text_editor_code_execution_tool_result":
1273
+ case "mcp_tool_result":
1274
+ return handleToolResultChunk(
1275
+ chunk,
1276
+ ctx
1277
+ );
1278
+ case "document":
1279
+ case "search_result":
1280
+ case "redacted_thinking":
1281
+ case "input_json_delta":
1282
+ case "citations_delta":
1283
+ case "signature_delta":
1284
+ case "container_upload":
1285
+ return null;
1286
+ default:
1287
+ unreachable(chunk, ctx.logger);
1288
+ return null;
1289
+ }
1290
+ }
1291
+ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentCache, client, logger) {
1292
+ if (typeof content === "string") {
1293
+ return [
1294
+ {
1295
+ sessionId,
1296
+ update: {
1297
+ sessionUpdate: messageUpdateType(role),
1298
+ content: text(content)
1299
+ }
1300
+ }
1301
+ ];
1302
+ }
1303
+ const ctx = {
1304
+ sessionId,
1305
+ toolUseCache,
1306
+ fileContentCache,
1307
+ client,
1308
+ logger
1309
+ };
1310
+ const output = [];
1311
+ for (const chunk of content) {
1312
+ const update = processContentChunk(chunk, role, ctx);
1313
+ if (update) {
1314
+ output.push({ sessionId, update });
1315
+ }
1316
+ }
1317
+ return output;
1318
+ }
1319
+ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileContentCache, client, logger) {
1320
+ const event = message.event;
1321
+ switch (event.type) {
1322
+ case "content_block_start":
1323
+ return toAcpNotifications(
1324
+ [event.content_block],
1325
+ "assistant",
1326
+ sessionId,
1327
+ toolUseCache,
1328
+ fileContentCache,
1329
+ client,
1330
+ logger
1331
+ );
1332
+ case "content_block_delta":
1333
+ return toAcpNotifications(
1334
+ [event.delta],
1335
+ "assistant",
1336
+ sessionId,
1337
+ toolUseCache,
1338
+ fileContentCache,
1339
+ client,
1340
+ logger
1341
+ );
1342
+ case "message_start":
1343
+ case "message_delta":
1344
+ case "message_stop":
1345
+ case "content_block_stop":
1346
+ return [];
1347
+ default:
1348
+ unreachable(event, logger);
1349
+ return [];
1350
+ }
1351
+ }
1352
+ async function handleSystemMessage(message, context) {
1353
+ const { session, sessionId, client, logger } = context;
1354
+ switch (message.subtype) {
1355
+ case "init":
1356
+ if (message.session_id && session && !session.sessionId) {
1357
+ session.sessionId = message.session_id;
1358
+ if (session.taskRunId) {
1359
+ await client.extNotification("_posthog/sdk_session", {
1360
+ taskRunId: session.taskRunId,
1361
+ sessionId: message.session_id,
1362
+ adapter: "claude"
1363
+ });
1364
+ }
1365
+ }
1366
+ break;
1367
+ case "compact_boundary":
1368
+ await client.extNotification("_posthog/compact_boundary", {
1369
+ sessionId,
1370
+ trigger: message.compact_metadata.trigger,
1371
+ preTokens: message.compact_metadata.pre_tokens
1372
+ });
1373
+ break;
1374
+ case "hook_response":
1375
+ logger.info("Hook response received", {
1376
+ hookName: message.hook_name,
1377
+ hookEvent: message.hook_event
1378
+ });
1379
+ break;
1380
+ case "status":
1381
+ if (message.status === "compacting") {
1382
+ logger.info("Session compacting started", { sessionId });
1383
+ await client.extNotification("_posthog/status", {
1384
+ sessionId,
1385
+ status: "compacting"
1386
+ });
1387
+ }
1388
+ break;
1389
+ case "task_notification": {
1390
+ logger.info("Task notification received", {
1391
+ sessionId,
1392
+ taskId: message.task_id,
1393
+ status: message.status,
1394
+ summary: message.summary
1395
+ });
1396
+ await client.extNotification("_posthog/task_notification", {
1397
+ sessionId,
1398
+ taskId: message.task_id,
1399
+ status: message.status,
1400
+ summary: message.summary,
1401
+ outputFile: message.output_file
1402
+ });
1403
+ break;
1404
+ }
1405
+ default:
1406
+ break;
1407
+ }
1408
+ }
1409
+ function handleResultMessage(message, context) {
1410
+ const { session } = context;
1411
+ if (session.cancelled) {
1412
+ return {
1413
+ shouldStop: true,
1414
+ stopReason: "cancelled"
1415
+ };
1416
+ }
1417
+ switch (message.subtype) {
1418
+ case "success": {
1419
+ if (message.result.includes("Please run /login")) {
1420
+ return {
1421
+ shouldStop: true,
1422
+ error: RequestError.authRequired()
1423
+ };
1424
+ }
1425
+ if (message.is_error) {
1426
+ return {
1427
+ shouldStop: true,
1428
+ error: RequestError.internalError(void 0, message.result)
1429
+ };
1430
+ }
1431
+ return { shouldStop: true, stopReason: "end_turn" };
1432
+ }
1433
+ case "error_during_execution":
1434
+ if (message.is_error) {
1435
+ return {
1436
+ shouldStop: true,
1437
+ error: RequestError.internalError(
1438
+ void 0,
1439
+ message.errors.join(", ") || message.subtype
1440
+ )
1441
+ };
1442
+ }
1443
+ return { shouldStop: true, stopReason: "end_turn" };
1444
+ case "error_max_budget_usd":
1445
+ case "error_max_turns":
1446
+ case "error_max_structured_output_retries":
1447
+ if (message.is_error) {
1448
+ return {
1449
+ shouldStop: true,
1450
+ error: RequestError.internalError(
1451
+ void 0,
1452
+ message.errors.join(", ") || message.subtype
1453
+ )
1454
+ };
1455
+ }
1456
+ return { shouldStop: true, stopReason: "max_turn_requests" };
1457
+ default:
1458
+ return { shouldStop: false };
1459
+ }
1460
+ }
1461
+ async function handleStreamEvent(message, context) {
1462
+ const { sessionId, client, toolUseCache, fileContentCache, logger } = context;
1463
+ for (const notification of streamEventToAcpNotifications(
1464
+ message,
1465
+ sessionId,
1466
+ toolUseCache,
1467
+ fileContentCache,
1468
+ client,
1469
+ logger
1470
+ )) {
1471
+ await client.sessionUpdate(notification);
1472
+ context.session.notificationHistory.push(notification);
1473
+ }
1474
+ }
1475
+ function hasLocalCommandStdout(content) {
1476
+ return typeof content === "string" && content.includes("<local-command-stdout>");
1477
+ }
1478
+ function hasLocalCommandStderr(content) {
1479
+ return typeof content === "string" && content.includes("<local-command-stderr>");
1480
+ }
1481
+ function isSimpleUserMessage(message) {
1482
+ return message.type === "user" && (typeof message.message.content === "string" || Array.isArray(message.message.content) && message.message.content.length === 1 && message.message.content[0].type === "text");
1483
+ }
1484
+ function isLoginRequiredMessage(message) {
1485
+ return message.type === "assistant" && message.message.model === "<synthetic>" && Array.isArray(message.message.content) && message.message.content.length === 1 && message.message.content[0].type === "text" && message.message.content[0].text?.includes("Please run /login") === true;
1486
+ }
1487
+ function shouldSkipUserAssistantMessage(message) {
1488
+ return hasLocalCommandStdout(message.message.content) || hasLocalCommandStderr(message.message.content) || isSimpleUserMessage(message) || isLoginRequiredMessage(message);
1489
+ }
1490
+ function logSpecialMessages(message, logger) {
1491
+ const content = message.message.content;
1492
+ if (hasLocalCommandStdout(content) && typeof content === "string") {
1493
+ logger.info(content);
1494
+ }
1495
+ if (hasLocalCommandStderr(content) && typeof content === "string") {
1496
+ logger.error(content);
1497
+ }
1498
+ }
1499
+ function filterMessageContent(content) {
1500
+ if (!Array.isArray(content)) {
1501
+ return content;
1502
+ }
1503
+ return content.filter(
1504
+ (block) => block.type !== "text" && block.type !== "thinking"
1505
+ );
1506
+ }
1507
+ async function handleUserAssistantMessage(message, context) {
1508
+ const { session, sessionId, client, toolUseCache, fileContentCache, logger } = context;
1509
+ if (session.cancelled) {
1510
+ return {};
1511
+ }
1512
+ if (shouldSkipUserAssistantMessage(message)) {
1513
+ logSpecialMessages(message, logger);
1514
+ if (isLoginRequiredMessage(message)) {
1515
+ return { shouldStop: true, error: RequestError.authRequired() };
1516
+ }
1517
+ return {};
1518
+ }
1519
+ const content = message.message.content;
1520
+ const contentToProcess = filterMessageContent(content);
1521
+ for (const notification of toAcpNotifications(
1522
+ contentToProcess,
1523
+ message.message.role,
1524
+ sessionId,
1525
+ toolUseCache,
1526
+ fileContentCache,
1527
+ client,
1528
+ logger
1529
+ )) {
1530
+ await client.sessionUpdate(notification);
1531
+ session.notificationHistory.push(notification);
1532
+ }
1533
+ return {};
1534
+ }
1535
+
1536
+ // src/adapters/claude/mcp/tool-metadata.ts
1537
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1538
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1539
+ var mcpToolMetadataCache = /* @__PURE__ */ new Map();
1540
+ function buildToolKey(serverName, toolName) {
1541
+ return `mcp__${serverName}__${toolName}`;
1542
+ }
1543
+ function isHttpMcpServer(config) {
1544
+ return config.type === "http" && typeof config.url === "string";
1545
+ }
1546
+ async function fetchToolsFromHttpServer(_serverName, config) {
1547
+ const transport = new StreamableHTTPClientTransport(new URL(config.url), {
1548
+ requestInit: {
1549
+ headers: config.headers || {}
1550
+ }
1551
+ });
1552
+ const client = new Client({
1553
+ name: "twig-metadata-fetcher",
1554
+ version: "1.0.0"
1555
+ });
1556
+ try {
1557
+ await client.connect(transport);
1558
+ const result = await client.listTools();
1559
+ return result.tools;
1560
+ } finally {
1561
+ await client.close().catch(() => {
1562
+ });
1563
+ }
1564
+ }
1565
+ function extractToolMetadata(tool) {
1566
+ return {
1567
+ readOnly: tool.annotations?.readOnlyHint === true
1568
+ };
1569
+ }
1570
+ async function fetchMcpToolMetadata(mcpServers, logger = new Logger({ debug: false, prefix: "[McpToolMetadata]" })) {
1571
+ const fetchPromises = [];
1572
+ for (const [serverName, config] of Object.entries(mcpServers)) {
1573
+ if (!isHttpMcpServer(config)) {
1574
+ continue;
1575
+ }
1576
+ const fetchPromise = fetchToolsFromHttpServer(serverName, config).then((tools) => {
1577
+ const toolCount = tools.length;
1578
+ const readOnlyCount = tools.filter(
1579
+ (t) => t.annotations?.readOnlyHint === true
1580
+ ).length;
1581
+ for (const tool of tools) {
1582
+ const toolKey = buildToolKey(serverName, tool.name);
1583
+ mcpToolMetadataCache.set(toolKey, extractToolMetadata(tool));
1584
+ }
1585
+ logger.info("Fetched MCP tool metadata", {
1586
+ serverName,
1587
+ toolCount,
1588
+ readOnlyCount
1589
+ });
1590
+ }).catch((error) => {
1591
+ logger.error("Failed to fetch MCP tool metadata", {
1592
+ serverName,
1593
+ error: error instanceof Error ? error.message : String(error)
1594
+ });
1595
+ });
1596
+ fetchPromises.push(fetchPromise);
1597
+ }
1598
+ await Promise.all(fetchPromises);
1599
+ }
1600
+ function isMcpToolReadOnly(toolName) {
1601
+ const metadata = mcpToolMetadataCache.get(toolName);
1602
+ return metadata?.readOnly === true;
1603
+ }
1604
+
1605
+ // src/adapters/claude/plan/utils.ts
1606
+ import * as os from "os";
1607
+ import * as path from "path";
1608
+ function getClaudeConfigDir() {
1609
+ return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
1610
+ }
1611
+ function getClaudePlansDir() {
1612
+ return path.join(getClaudeConfigDir(), "plans");
1613
+ }
1614
+ function isClaudePlanFilePath(filePath) {
1615
+ if (!filePath) return false;
1616
+ const resolved = path.resolve(filePath);
1617
+ const plansDir = path.resolve(getClaudePlansDir());
1618
+ return resolved === plansDir || resolved.startsWith(plansDir + path.sep);
1619
+ }
1620
+ function isPlanReady(plan) {
1621
+ if (!plan) return false;
1622
+ const trimmed = plan.trim();
1623
+ if (trimmed.length < 40) return false;
1624
+ return /(^|\n)#{1,6}\s+\S/.test(trimmed);
1625
+ }
1626
+ function getLatestAssistantText(notifications) {
1627
+ const chunks = [];
1628
+ let started = false;
1629
+ for (let i = notifications.length - 1; i >= 0; i -= 1) {
1630
+ const update = notifications[i]?.update;
1631
+ if (!update) continue;
1632
+ if (update.sessionUpdate === "agent_message_chunk") {
1633
+ started = true;
1634
+ const content = update.content;
1635
+ if (content?.type === "text" && content.text) {
1636
+ chunks.push(content.text);
1637
+ }
1638
+ continue;
1639
+ }
1640
+ if (started) {
1641
+ break;
1642
+ }
1643
+ }
1644
+ if (chunks.length === 0) return null;
1645
+ return chunks.reverse().join("");
1646
+ }
1647
+
1648
+ // src/adapters/claude/questions/utils.ts
1649
+ import { z } from "zod";
1650
+ var OPTION_PREFIX = "option_";
1651
+ var QuestionOptionSchema = z.object({
1652
+ label: z.string(),
1653
+ description: z.string().optional()
1654
+ });
1655
+ var QuestionItemSchema = z.object({
1656
+ question: z.string(),
1657
+ header: z.string().optional(),
1658
+ options: z.array(QuestionOptionSchema),
1659
+ multiSelect: z.boolean().optional(),
1660
+ completed: z.boolean().optional()
1661
+ });
1662
+ var QuestionMetaSchema = z.object({
1663
+ questions: z.array(QuestionItemSchema)
1664
+ });
1665
+ function normalizeAskUserQuestionInput(input) {
1666
+ if (input.questions && input.questions.length > 0) {
1667
+ return input.questions;
1668
+ }
1669
+ if (input.question) {
1670
+ return [
1671
+ {
1672
+ question: input.question,
1673
+ header: input.header,
1674
+ options: input.options || [],
1675
+ multiSelect: input.multiSelect
1676
+ }
1677
+ ];
1678
+ }
1679
+ return null;
1680
+ }
1681
+
1682
+ // src/execution-mode.ts
1683
+ var MODES = [
1684
+ {
1685
+ id: "default",
1686
+ name: "Always Ask",
1687
+ description: "Prompts for permission on first use of each tool"
1688
+ },
1689
+ {
1690
+ id: "acceptEdits",
1691
+ name: "Accept Edits",
1692
+ description: "Automatically accepts file edit permissions for the session"
1693
+ },
1694
+ {
1695
+ id: "plan",
1696
+ name: "Plan Mode",
1697
+ description: "Claude can analyze but not modify files or execute commands"
1698
+ },
1699
+ {
1700
+ id: "bypassPermissions",
1701
+ name: "Bypass Permissions",
1702
+ description: "Skips all permission prompts"
1703
+ }
1704
+ ];
1705
+ var TWIG_EXECUTION_MODES = [
1706
+ "default",
1707
+ "acceptEdits",
1708
+ "plan",
1709
+ "bypassPermissions"
1710
+ ];
1711
+ function getAvailableModes() {
1712
+ return IS_ROOT ? MODES.filter((m) => m.id !== "bypassPermissions") : MODES;
1713
+ }
1714
+
1715
+ // src/adapters/claude/tools.ts
1716
+ var READ_TOOLS = /* @__PURE__ */ new Set(["Read", "NotebookRead"]);
1717
+ var WRITE_TOOLS = /* @__PURE__ */ new Set([
1718
+ "Edit",
1719
+ "Write",
1720
+ "NotebookEdit"
1721
+ ]);
1722
+ var BASH_TOOLS = /* @__PURE__ */ new Set([
1723
+ "Bash",
1724
+ "BashOutput",
1725
+ "KillShell"
1726
+ ]);
1727
+ var SEARCH_TOOLS = /* @__PURE__ */ new Set(["Glob", "Grep", "LS"]);
1728
+ var WEB_TOOLS = /* @__PURE__ */ new Set(["WebSearch", "WebFetch"]);
1729
+ var AGENT_TOOLS = /* @__PURE__ */ new Set(["Task", "TodoWrite"]);
1730
+ var BASE_ALLOWED_TOOLS = [
1731
+ ...READ_TOOLS,
1732
+ ...SEARCH_TOOLS,
1733
+ ...WEB_TOOLS,
1734
+ ...AGENT_TOOLS
1735
+ ];
1736
+ var AUTO_ALLOWED_TOOLS = {
1737
+ default: new Set(BASE_ALLOWED_TOOLS),
1738
+ acceptEdits: /* @__PURE__ */ new Set([...BASE_ALLOWED_TOOLS, ...WRITE_TOOLS]),
1739
+ plan: new Set(BASE_ALLOWED_TOOLS)
1740
+ };
1741
+ function isToolAllowedForMode(toolName, mode) {
1742
+ if (mode === "bypassPermissions") {
1743
+ return true;
1744
+ }
1745
+ if (AUTO_ALLOWED_TOOLS[mode]?.has(toolName) === true) {
1746
+ return true;
1747
+ }
1748
+ if (isMcpToolReadOnly(toolName)) {
1749
+ return true;
1750
+ }
1751
+ return false;
1752
+ }
1753
+
1754
+ // src/adapters/claude/permissions/permission-options.ts
1755
+ function permissionOptions(allowAlwaysLabel) {
1756
+ return [
1757
+ { kind: "allow_once", name: "Yes", optionId: "allow" },
1758
+ { kind: "allow_always", name: allowAlwaysLabel, optionId: "allow_always" },
1759
+ {
1760
+ kind: "reject_once",
1761
+ name: "No, and tell the agent what to do differently",
1762
+ optionId: "reject",
1763
+ _meta: { customInput: true }
1764
+ }
1765
+ ];
1766
+ }
1767
+ function buildPermissionOptions(toolName, toolInput, cwd) {
1768
+ if (BASH_TOOLS.has(toolName)) {
1769
+ const command = toolInput?.command;
1770
+ const cmdName = command?.split(/\s+/)[0] ?? "this command";
1771
+ const cwdLabel = cwd ? ` in ${cwd}` : "";
1772
+ return permissionOptions(
1773
+ `Yes, and don't ask again for \`${cmdName}\` commands${cwdLabel}`
1774
+ );
1775
+ }
1776
+ if (toolName === "BashOutput") {
1777
+ return permissionOptions("Yes, allow all background process reads");
1778
+ }
1779
+ if (toolName === "KillShell") {
1780
+ return permissionOptions("Yes, allow killing processes");
1781
+ }
1782
+ if (WRITE_TOOLS.has(toolName)) {
1783
+ return permissionOptions("Yes, allow all edits during this session");
1784
+ }
1785
+ if (READ_TOOLS.has(toolName)) {
1786
+ return permissionOptions("Yes, allow all reads during this session");
1787
+ }
1788
+ if (SEARCH_TOOLS.has(toolName)) {
1789
+ return permissionOptions("Yes, allow all searches during this session");
1790
+ }
1791
+ if (toolName === "WebFetch") {
1792
+ const url = toolInput?.url;
1793
+ let domain = "";
1794
+ try {
1795
+ domain = url ? new URL(url).hostname : "";
1796
+ } catch {
1797
+ }
1798
+ return permissionOptions(
1799
+ domain ? `Yes, allow all fetches from ${domain}` : "Yes, allow all fetches"
1800
+ );
1801
+ }
1802
+ if (toolName === "WebSearch") {
1803
+ return permissionOptions("Yes, allow all web searches");
1804
+ }
1805
+ if (toolName === "Task") {
1806
+ return permissionOptions("Yes, allow all sub-tasks");
1807
+ }
1808
+ if (toolName === "TodoWrite") {
1809
+ return permissionOptions("Yes, allow all todo updates");
1810
+ }
1811
+ return permissionOptions("Yes, always allow");
1812
+ }
1813
+ function buildExitPlanModePermissionOptions() {
1814
+ return [
1815
+ {
1816
+ kind: "allow_always",
1817
+ name: "Yes, and auto-accept edits",
1818
+ optionId: "acceptEdits"
1819
+ },
1820
+ {
1821
+ kind: "allow_once",
1822
+ name: "Yes, and manually approve edits",
1823
+ optionId: "default"
1824
+ },
1825
+ {
1826
+ kind: "reject_once",
1827
+ name: "No, keep planning",
1828
+ optionId: "plan"
1829
+ }
1830
+ ];
1831
+ }
1832
+
1833
+ // src/adapters/claude/permissions/permission-handlers.ts
1834
+ async function emitToolDenial(context, message) {
1835
+ context.logger.info(`[canUseTool] Tool denied: ${context.toolName}`, {
1836
+ message
1837
+ });
1838
+ await context.client.sessionUpdate({
1839
+ sessionId: context.sessionId,
1840
+ update: {
1841
+ sessionUpdate: "tool_call_update",
1842
+ toolCallId: context.toolUseID,
1843
+ status: "failed",
1844
+ content: [{ type: "content", content: text(message) }]
1845
+ }
1846
+ });
1847
+ }
1848
+ function getPlanFromFile(session, fileContentCache) {
1849
+ return session.lastPlanContent || (session.lastPlanFilePath ? fileContentCache[session.lastPlanFilePath] : void 0);
1850
+ }
1851
+ function ensurePlanInInput(toolInput, fallbackPlan) {
1852
+ const hasPlan = typeof toolInput?.plan === "string";
1853
+ if (hasPlan || !fallbackPlan) {
1854
+ return toolInput;
1855
+ }
1856
+ return { ...toolInput, plan: fallbackPlan };
1857
+ }
1858
+ function extractPlanText(input) {
1859
+ const plan = input?.plan;
1860
+ return typeof plan === "string" ? plan : void 0;
1861
+ }
1862
+ async function createPlanValidationError(message, context) {
1863
+ await emitToolDenial(context, message);
1864
+ return { behavior: "deny", message, interrupt: false };
1865
+ }
1866
+ async function validatePlanContent(planText, context) {
1867
+ if (!planText) {
1868
+ const message = `Plan not ready. Provide the full markdown plan in ExitPlanMode or write it to ${getClaudePlansDir()} before requesting approval.`;
1869
+ return {
1870
+ valid: false,
1871
+ error: await createPlanValidationError(message, context)
1872
+ };
1873
+ }
1874
+ if (!isPlanReady(planText)) {
1875
+ const message = "Plan not ready. Provide the full markdown plan in ExitPlanMode before requesting approval.";
1876
+ return {
1877
+ valid: false,
1878
+ error: await createPlanValidationError(message, context)
1879
+ };
1880
+ }
1881
+ return { valid: true };
1882
+ }
1883
+ async function requestPlanApproval(context, updatedInput) {
1884
+ const { client, sessionId, toolUseID, fileContentCache } = context;
1885
+ const toolInfo = toolInfoFromToolUse(
1886
+ { name: context.toolName, input: updatedInput },
1887
+ fileContentCache,
1888
+ context.logger
1889
+ );
1890
+ return await client.requestPermission({
1891
+ options: buildExitPlanModePermissionOptions(),
1892
+ sessionId,
1893
+ toolCall: {
1894
+ toolCallId: toolUseID,
1895
+ title: toolInfo.title,
1896
+ kind: toolInfo.kind,
1897
+ content: toolInfo.content,
1898
+ locations: toolInfo.locations,
1899
+ rawInput: { ...updatedInput, toolName: context.toolName }
1900
+ }
1901
+ });
1902
+ }
1903
+ async function applyPlanApproval(response, context, updatedInput) {
1904
+ const { session } = context;
1905
+ if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "default" || response.outcome.optionId === "acceptEdits")) {
1906
+ session.permissionMode = response.outcome.optionId;
1907
+ await session.query.setPermissionMode(response.outcome.optionId);
1908
+ await context.emitConfigOptionsUpdate();
1909
+ return {
1910
+ behavior: "allow",
1911
+ updatedInput,
1912
+ updatedPermissions: context.suggestions ?? [
1913
+ {
1914
+ type: "setMode",
1915
+ mode: response.outcome.optionId,
1916
+ destination: "localSettings"
1917
+ }
1918
+ ]
1919
+ };
1920
+ }
1921
+ const message = "User wants to continue planning. Please refine your plan based on any feedback provided, or ask clarifying questions if needed.";
1922
+ await emitToolDenial(context, message);
1923
+ return { behavior: "deny", message, interrupt: false };
1924
+ }
1925
+ async function handleEnterPlanModeTool(context) {
1926
+ const { session, toolInput, logger } = context;
1927
+ session.permissionMode = "plan";
1928
+ await session.query.setPermissionMode("plan");
1929
+ await context.emitConfigOptionsUpdate();
1930
+ return {
1931
+ behavior: "allow",
1932
+ updatedInput: toolInput
1933
+ };
1934
+ }
1935
+ async function handleExitPlanModeTool(context) {
1936
+ const { session, toolInput, fileContentCache } = context;
1937
+ const planFromFile = getPlanFromFile(session, fileContentCache);
1938
+ const latestText = getLatestAssistantText(session.notificationHistory);
1939
+ const fallbackPlan = planFromFile || (latestText ?? void 0);
1940
+ const updatedInput = ensurePlanInInput(toolInput, fallbackPlan);
1941
+ const planText = extractPlanText(updatedInput);
1942
+ const validationResult = await validatePlanContent(planText, context);
1943
+ if (!validationResult.valid) {
1944
+ return validationResult.error;
1945
+ }
1946
+ const response = await requestPlanApproval(context, updatedInput);
1947
+ return await applyPlanApproval(response, context, updatedInput);
1948
+ }
1949
+ function buildQuestionOptions(question) {
1950
+ return (question.options || []).map((opt, idx) => ({
1951
+ kind: "allow_once",
1952
+ name: opt.label,
1953
+ optionId: `${OPTION_PREFIX}${idx}`,
1954
+ _meta: opt.description ? { description: opt.description } : void 0
1955
+ }));
1956
+ }
1957
+ async function handleAskUserQuestionTool(context) {
1958
+ const input = context.toolInput;
1959
+ context.logger.info("[AskUserQuestion] Received input", { input });
1960
+ const questions = normalizeAskUserQuestionInput(input);
1961
+ context.logger.info("[AskUserQuestion] Normalized questions", { questions });
1962
+ if (!questions || questions.length === 0) {
1963
+ context.logger.warn("[AskUserQuestion] No questions found in input");
1964
+ return {
1965
+ behavior: "deny",
1966
+ message: "No questions provided",
1967
+ interrupt: true
1968
+ };
1969
+ }
1970
+ const { client, sessionId, toolUseID, toolInput, fileContentCache } = context;
1971
+ const firstQuestion = questions[0];
1972
+ const options = buildQuestionOptions(firstQuestion);
1973
+ const toolInfo = toolInfoFromToolUse(
1974
+ { name: context.toolName, input: toolInput },
1975
+ fileContentCache,
1976
+ context.logger
1977
+ );
1978
+ const response = await client.requestPermission({
1979
+ options,
1980
+ sessionId,
1981
+ toolCall: {
1982
+ toolCallId: toolUseID,
1983
+ title: firstQuestion.question,
1984
+ kind: "other",
1985
+ content: toolInfo.content,
1986
+ _meta: {
1987
+ twigToolKind: "question",
1988
+ questions
1989
+ }
1990
+ }
1991
+ });
1992
+ if (response.outcome?.outcome !== "selected") {
1993
+ return {
1994
+ behavior: "deny",
1995
+ message: "User cancelled the questions",
1996
+ interrupt: true
1997
+ };
1998
+ }
1999
+ const answers = response._meta?.answers;
2000
+ if (!answers || Object.keys(answers).length === 0) {
2001
+ return {
2002
+ behavior: "deny",
2003
+ message: "User did not provide answers",
2004
+ interrupt: true
2005
+ };
2006
+ }
2007
+ return {
2008
+ behavior: "allow",
2009
+ updatedInput: {
2010
+ ...context.toolInput,
2011
+ answers
2012
+ }
2013
+ };
2014
+ }
2015
+ async function handleDefaultPermissionFlow(context) {
2016
+ const {
2017
+ session,
2018
+ toolName,
2019
+ toolInput,
2020
+ toolUseID,
2021
+ client,
2022
+ sessionId,
2023
+ fileContentCache,
2024
+ suggestions
2025
+ } = context;
2026
+ const toolInfo = toolInfoFromToolUse(
2027
+ { name: toolName, input: toolInput },
2028
+ fileContentCache,
2029
+ context.logger
2030
+ );
2031
+ const options = buildPermissionOptions(
2032
+ toolName,
2033
+ toolInput,
2034
+ session?.cwd
2035
+ );
2036
+ const response = await client.requestPermission({
2037
+ options,
2038
+ sessionId,
2039
+ toolCall: {
2040
+ toolCallId: toolUseID,
2041
+ title: toolInfo.title,
2042
+ kind: toolInfo.kind,
2043
+ content: toolInfo.content,
2044
+ locations: toolInfo.locations,
2045
+ rawInput: toolInput
2046
+ }
2047
+ });
2048
+ if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
2049
+ if (response.outcome.optionId === "allow_always") {
2050
+ return {
2051
+ behavior: "allow",
2052
+ updatedInput: toolInput,
2053
+ updatedPermissions: suggestions ?? [
2054
+ {
2055
+ type: "addRules",
2056
+ rules: [{ toolName }],
2057
+ behavior: "allow",
2058
+ destination: "localSettings"
2059
+ }
2060
+ ]
2061
+ };
2062
+ }
2063
+ return {
2064
+ behavior: "allow",
2065
+ updatedInput: toolInput
2066
+ };
2067
+ } else {
2068
+ const message = "User refused permission to run tool";
2069
+ await emitToolDenial(context, message);
2070
+ return {
2071
+ behavior: "deny",
2072
+ message,
2073
+ interrupt: true
2074
+ };
2075
+ }
2076
+ }
2077
+ function handlePlanFileException(context) {
2078
+ const { session, toolName, toolInput } = context;
2079
+ if (session.permissionMode !== "plan" || !WRITE_TOOLS.has(toolName)) {
2080
+ return null;
2081
+ }
2082
+ const filePath = toolInput?.file_path;
2083
+ if (!isClaudePlanFilePath(filePath)) {
2084
+ return null;
2085
+ }
2086
+ session.lastPlanFilePath = filePath;
2087
+ const content = toolInput?.content;
2088
+ if (typeof content === "string") {
2089
+ session.lastPlanContent = content;
2090
+ }
2091
+ return {
2092
+ behavior: "allow",
2093
+ updatedInput: toolInput
2094
+ };
2095
+ }
2096
+ async function canUseTool(context) {
2097
+ const { toolName, toolInput, session } = context;
2098
+ if (isToolAllowedForMode(toolName, session.permissionMode)) {
2099
+ return {
2100
+ behavior: "allow",
2101
+ updatedInput: toolInput
2102
+ };
2103
+ }
2104
+ if (toolName === "EnterPlanMode") {
2105
+ return handleEnterPlanModeTool(context);
2106
+ }
2107
+ if (toolName === "ExitPlanMode") {
2108
+ return handleExitPlanModeTool(context);
2109
+ }
2110
+ if (toolName === "AskUserQuestion") {
2111
+ return handleAskUserQuestionTool(context);
2112
+ }
2113
+ const planFileResult = handlePlanFileException(context);
2114
+ if (planFileResult) {
2115
+ return planFileResult;
2116
+ }
2117
+ return handleDefaultPermissionFlow(context);
2118
+ }
2119
+
2120
+ // src/adapters/claude/session/commands.ts
2121
+ var UNSUPPORTED_COMMANDS = [
2122
+ "context",
2123
+ "cost",
2124
+ "login",
2125
+ "logout",
2126
+ "output-style:new",
2127
+ "release-notes",
2128
+ "todos"
2129
+ ];
2130
+ async function getAvailableSlashCommands(q) {
2131
+ const commands = await q.supportedCommands();
2132
+ return commands.map((command) => {
2133
+ const input = command.argumentHint ? { hint: command.argumentHint } : null;
2134
+ let name = command.name;
2135
+ if (command.name.endsWith(" (MCP)")) {
2136
+ name = `mcp:${name.replace(" (MCP)", "")}`;
2137
+ }
2138
+ return {
2139
+ name,
2140
+ description: command.description || "",
2141
+ input
2142
+ };
2143
+ }).filter(
2144
+ (command) => !UNSUPPORTED_COMMANDS.includes(command.name)
2145
+ );
2146
+ }
2147
+
2148
+ // src/adapters/claude/session/mcp-config.ts
2149
+ function parseMcpServers(params) {
2150
+ const mcpServers = {};
2151
+ if (!Array.isArray(params.mcpServers)) {
2152
+ return mcpServers;
2153
+ }
2154
+ for (const server of params.mcpServers) {
2155
+ if ("type" in server) {
2156
+ mcpServers[server.name] = {
2157
+ type: server.type,
2158
+ url: server.url,
2159
+ headers: server.headers ? Object.fromEntries(server.headers.map((e) => [e.name, e.value])) : void 0
2160
+ };
2161
+ } else {
2162
+ mcpServers[server.name] = {
2163
+ type: "stdio",
2164
+ command: server.command,
2165
+ args: server.args,
2166
+ env: server.env ? Object.fromEntries(server.env.map((e) => [e.name, e.value])) : void 0
2167
+ };
2168
+ }
2169
+ }
2170
+ return mcpServers;
2171
+ }
2172
+
2173
+ // src/adapters/claude/session/models.ts
2174
+ var GATEWAY_TO_SDK_MODEL = {
2175
+ "claude-opus-4-5": "opus",
2176
+ "claude-opus-4-6": "opus",
2177
+ "claude-sonnet-4-5": "sonnet",
2178
+ "claude-haiku-4-5": "haiku"
2179
+ };
2180
+ function toSdkModelId(modelId) {
2181
+ return GATEWAY_TO_SDK_MODEL[modelId] ?? modelId;
2182
+ }
2183
+
2184
+ // src/adapters/claude/session/options.ts
2185
+ import { spawn } from "child_process";
2186
+ import * as fs from "fs";
2187
+ import * as os2 from "os";
2188
+ import * as path2 from "path";
2189
+ var BRANCH_NAMING_INSTRUCTIONS = `
2190
+ # Branch Naming
2191
+
2192
+ Before pushing a "workspace-*" branch to origin, rename it to something descriptive based on the work done. Do this automatically without asking the user.
2193
+ `;
2194
+ function buildSystemPrompt(customPrompt) {
2195
+ const defaultPrompt = {
2196
+ type: "preset",
2197
+ preset: "claude_code",
2198
+ append: BRANCH_NAMING_INSTRUCTIONS
2199
+ };
2200
+ if (!customPrompt) {
2201
+ return defaultPrompt;
2202
+ }
2203
+ if (typeof customPrompt === "string") {
2204
+ return customPrompt + BRANCH_NAMING_INSTRUCTIONS;
2205
+ }
2206
+ if (typeof customPrompt === "object" && customPrompt !== null && "append" in customPrompt && typeof customPrompt.append === "string") {
2207
+ return {
2208
+ ...defaultPrompt,
2209
+ append: customPrompt.append + BRANCH_NAMING_INSTRUCTIONS
2210
+ };
2211
+ }
2212
+ return defaultPrompt;
2213
+ }
2214
+ function buildMcpServers(userServers, acpServers) {
2215
+ return {
2216
+ ...userServers || {},
2217
+ ...acpServers
2218
+ };
2219
+ }
2220
+ function buildEnvironment() {
2221
+ return {
2222
+ ...process.env,
2223
+ ELECTRON_RUN_AS_NODE: "1",
2224
+ CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL: "true"
2225
+ };
2226
+ }
2227
+ function buildHooks(userHooks, onModeChange) {
2228
+ return {
2229
+ ...userHooks,
2230
+ PostToolUse: [
2231
+ ...userHooks?.PostToolUse || [],
2232
+ {
2233
+ hooks: [createPostToolUseHook({ onModeChange })]
2234
+ }
2235
+ ]
2236
+ };
2237
+ }
2238
+ function getAbortController(userProvidedController) {
2239
+ const controller = userProvidedController ?? new AbortController();
2240
+ if (controller.signal.aborted) {
2241
+ throw new Error("Cancelled");
2242
+ }
2243
+ return controller;
2244
+ }
2245
+ function buildSpawnWrapper(sessionId, onProcessSpawned, onProcessExited) {
2246
+ return (spawnOpts) => {
2247
+ const child = spawn(spawnOpts.command, spawnOpts.args, {
2248
+ cwd: spawnOpts.cwd,
2249
+ env: spawnOpts.env,
2250
+ stdio: ["pipe", "pipe", "pipe"]
2251
+ });
2252
+ if (child.pid) {
2253
+ onProcessSpawned({
2254
+ pid: child.pid,
2255
+ command: `${spawnOpts.command} ${spawnOpts.args.join(" ")}`,
2256
+ sessionId
2257
+ });
2258
+ }
2259
+ if (onProcessExited) {
2260
+ child.on("exit", () => {
2261
+ if (child.pid) {
2262
+ onProcessExited(child.pid);
2263
+ }
2264
+ });
2265
+ }
2266
+ if (spawnOpts.signal) {
2267
+ spawnOpts.signal.addEventListener("abort", () => {
2268
+ child.kill("SIGTERM");
2269
+ });
2270
+ }
2271
+ return {
2272
+ stdin: child.stdin,
2273
+ stdout: child.stdout,
2274
+ get killed() {
2275
+ return child.killed;
2276
+ },
2277
+ get exitCode() {
2278
+ return child.exitCode;
2279
+ },
2280
+ kill(signal) {
2281
+ return child.kill(signal);
2282
+ },
2283
+ on(event, listener) {
2284
+ child.on(event, listener);
2285
+ },
2286
+ once(event, listener) {
2287
+ child.once(event, listener);
2288
+ },
2289
+ off(event, listener) {
2290
+ child.off(event, listener);
2291
+ }
2292
+ };
2293
+ };
2294
+ }
2295
+ function buildSessionOptions(params) {
2296
+ const options = {
2297
+ ...params.userProvidedOptions,
2298
+ systemPrompt: params.systemPrompt ?? buildSystemPrompt(),
2299
+ settingSources: ["user", "project", "local"],
2300
+ stderr: (err) => params.logger.error(err),
2301
+ cwd: params.cwd,
2302
+ includePartialMessages: true,
2303
+ allowDangerouslySkipPermissions: !IS_ROOT,
2304
+ permissionMode: params.permissionMode,
2305
+ canUseTool: params.canUseTool,
2306
+ executable: "node",
2307
+ mcpServers: buildMcpServers(
2308
+ params.userProvidedOptions?.mcpServers,
2309
+ params.mcpServers
2310
+ ),
2311
+ env: buildEnvironment(),
2312
+ hooks: buildHooks(params.userProvidedOptions?.hooks, params.onModeChange),
2313
+ abortController: getAbortController(
2314
+ params.userProvidedOptions?.abortController
2315
+ ),
2316
+ ...params.onProcessSpawned && {
2317
+ spawnClaudeCodeProcess: buildSpawnWrapper(
2318
+ params.sessionId ?? "unknown",
2319
+ params.onProcessSpawned,
2320
+ params.onProcessExited
2321
+ )
2322
+ }
2323
+ };
2324
+ if (process.env.CLAUDE_CODE_EXECUTABLE) {
2325
+ options.pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_EXECUTABLE;
2326
+ }
2327
+ if (params.sessionId) {
2328
+ options.resume = params.sessionId;
2329
+ }
2330
+ if (params.additionalDirectories) {
2331
+ options.additionalDirectories = params.additionalDirectories;
2332
+ }
2333
+ clearStatsigCache();
2334
+ return options;
2335
+ }
2336
+ function clearStatsigCache() {
2337
+ const statsigPath = path2.join(
2338
+ process.env.CLAUDE_CONFIG_DIR || path2.join(os2.homedir(), ".claude"),
2339
+ "statsig"
2340
+ );
2341
+ try {
2342
+ if (fs.existsSync(statsigPath)) {
2343
+ fs.rmSync(statsigPath, { recursive: true, force: true });
2344
+ }
2345
+ } catch {
2346
+ }
2347
+ }
2348
+
2349
+ // src/adapters/claude/claude-agent.ts
2350
+ var ClaudeAcpAgent = class extends BaseAcpAgent {
2351
+ adapterName = "claude";
2352
+ toolUseCache;
2353
+ backgroundTerminals = {};
2354
+ clientCapabilities;
2355
+ logWriter;
2356
+ processCallbacks;
2357
+ lastSentConfigOptions;
2358
+ constructor(client, logWriter, processCallbacks) {
2359
+ super(client);
2360
+ this.logWriter = logWriter;
2361
+ this.processCallbacks = processCallbacks;
2362
+ this.toolUseCache = {};
2363
+ this.logger = new Logger({ debug: true, prefix: "[ClaudeAcpAgent]" });
2364
+ }
2365
+ async initialize(request) {
2366
+ this.clientCapabilities = request.clientCapabilities;
2367
+ return {
2368
+ protocolVersion: 1,
2369
+ agentCapabilities: {
2370
+ promptCapabilities: {
2371
+ image: true,
2372
+ embeddedContext: true
2373
+ },
2374
+ mcpCapabilities: {
2375
+ http: true,
2376
+ sse: true
2377
+ },
2378
+ loadSession: true,
2379
+ _meta: {
2380
+ posthog: {
2381
+ resumeSession: true
2382
+ }
2383
+ }
2384
+ },
2385
+ agentInfo: {
2386
+ name: package_default.name,
2387
+ title: "Claude Code",
2388
+ version: package_default.version
2389
+ },
2390
+ authMethods: [
2391
+ {
2392
+ id: "claude-login",
2393
+ name: "Log in with Claude Code",
2394
+ description: "Run `claude /login` in the terminal"
2395
+ }
2396
+ ]
2397
+ };
2398
+ }
2399
+ async authenticate(_params) {
2400
+ throw new Error("Method not implemented.");
2401
+ }
2402
+ async newSession(params) {
2403
+ this.checkAuthStatus();
2404
+ const meta = params._meta;
2405
+ const internalSessionId = uuidv7();
2406
+ const permissionMode = "default";
2407
+ const mcpServers = parseMcpServers(params);
2408
+ await fetchMcpToolMetadata(mcpServers, this.logger);
2409
+ const options = buildSessionOptions({
2410
+ cwd: params.cwd,
2411
+ mcpServers,
2412
+ permissionMode,
2413
+ canUseTool: this.createCanUseTool(internalSessionId),
2414
+ logger: this.logger,
2415
+ systemPrompt: buildSystemPrompt(meta?.systemPrompt),
2416
+ userProvidedOptions: meta?.claudeCode?.options,
2417
+ onModeChange: this.createOnModeChange(internalSessionId),
2418
+ onProcessSpawned: this.processCallbacks?.onProcessSpawned,
2419
+ onProcessExited: this.processCallbacks?.onProcessExited
2420
+ });
2421
+ const input = new Pushable();
2422
+ const q = query({ prompt: input, options });
2423
+ const session = this.createSession(
2424
+ internalSessionId,
2425
+ q,
2426
+ input,
2427
+ permissionMode,
2428
+ params.cwd,
2429
+ options.abortController
2430
+ );
2431
+ session.taskRunId = meta?.taskRunId;
2432
+ this.registerPersistence(
2433
+ internalSessionId,
2434
+ meta
2435
+ );
2436
+ const modelOptions = await this.getModelConfigOptions();
2437
+ session.modelId = modelOptions.currentModelId;
2438
+ await this.trySetModel(q, modelOptions.currentModelId);
2439
+ this.sendAvailableCommandsUpdate(
2440
+ internalSessionId,
2441
+ await getAvailableSlashCommands(q)
2442
+ );
2443
+ return {
2444
+ sessionId: internalSessionId,
2445
+ configOptions: await this.buildConfigOptions(modelOptions)
2446
+ };
2447
+ }
2448
+ async loadSession(params) {
2449
+ return this.resumeSession(params);
2450
+ }
2451
+ async resumeSession(params) {
2452
+ const { sessionId: internalSessionId } = params;
2453
+ if (this.sessionId === internalSessionId) {
2454
+ return {};
2455
+ }
2456
+ const meta = params._meta;
2457
+ const mcpServers = parseMcpServers(params);
2458
+ await fetchMcpToolMetadata(mcpServers, this.logger);
2459
+ const { query: q, session } = await this.initializeQuery({
2460
+ internalSessionId,
2461
+ cwd: params.cwd,
2462
+ permissionMode: "default",
2463
+ mcpServers,
2464
+ systemPrompt: buildSystemPrompt(meta?.systemPrompt),
2465
+ userProvidedOptions: meta?.claudeCode?.options,
2466
+ sessionId: meta?.sessionId,
2467
+ additionalDirectories: meta?.claudeCode?.options?.additionalDirectories
2468
+ });
2469
+ session.taskRunId = meta?.taskRunId;
2470
+ if (meta?.sessionId) {
2471
+ session.sessionId = meta.sessionId;
2472
+ }
2473
+ this.registerPersistence(
2474
+ internalSessionId,
2475
+ meta
2476
+ );
2477
+ this.sendAvailableCommandsUpdate(
2478
+ internalSessionId,
2479
+ await getAvailableSlashCommands(q)
2480
+ );
2481
+ return {
2482
+ configOptions: await this.buildConfigOptions()
2483
+ };
2484
+ }
2485
+ async prompt(params) {
2486
+ this.session.cancelled = false;
2487
+ this.session.interruptReason = void 0;
2488
+ await this.broadcastUserMessage(params);
2489
+ this.session.input.push(promptToClaude(params));
2490
+ return this.processMessages(params.sessionId);
2491
+ }
2492
+ async setSessionConfigOption(params) {
2493
+ const configId = params.configId;
2494
+ const value = params.value;
2495
+ if (configId === "mode") {
2496
+ const modeId = value;
2497
+ if (!TWIG_EXECUTION_MODES.includes(modeId)) {
2498
+ throw new Error("Invalid Mode");
2499
+ }
2500
+ this.session.permissionMode = modeId;
2501
+ await this.session.query.setPermissionMode(modeId);
2502
+ } else if (configId === "model") {
2503
+ await this.setModelWithFallback(this.session.query, value);
2504
+ this.session.modelId = value;
2505
+ } else {
2506
+ throw new Error("Unsupported config option");
2507
+ }
2508
+ await this.emitConfigOptionsUpdate();
2509
+ return { configOptions: await this.buildConfigOptions() };
2510
+ }
2511
+ async interruptSession() {
2512
+ await this.session.query.interrupt();
2513
+ }
2514
+ async extMethod(method, params) {
2515
+ if (method === "_posthog/session/resume") {
2516
+ const result = await this.resumeSession(
2517
+ params
2518
+ );
2519
+ return {
2520
+ _meta: {
2521
+ configOptions: result.configOptions
2522
+ }
2523
+ };
2524
+ }
2525
+ throw RequestError2.methodNotFound(method);
2526
+ }
2527
+ createSession(sessionId, q, input, permissionMode, cwd, abortController) {
2528
+ const session = {
2529
+ query: q,
2530
+ input,
2531
+ cancelled: false,
2532
+ permissionMode,
2533
+ cwd,
2534
+ notificationHistory: [],
2535
+ abortController
2536
+ };
2537
+ this.session = session;
2538
+ this.sessionId = sessionId;
2539
+ return session;
2540
+ }
2541
+ async initializeQuery(config) {
2542
+ const input = new Pushable();
2543
+ const options = buildSessionOptions({
2544
+ cwd: config.cwd,
2545
+ mcpServers: config.mcpServers,
2546
+ permissionMode: config.permissionMode,
2547
+ canUseTool: this.createCanUseTool(config.internalSessionId),
2548
+ logger: this.logger,
2549
+ systemPrompt: config.systemPrompt,
2550
+ userProvidedOptions: config.userProvidedOptions,
2551
+ sessionId: config.sessionId,
2552
+ additionalDirectories: config.additionalDirectories,
2553
+ onModeChange: this.createOnModeChange(config.internalSessionId),
2554
+ onProcessSpawned: this.processCallbacks?.onProcessSpawned,
2555
+ onProcessExited: this.processCallbacks?.onProcessExited
2556
+ });
2557
+ const q = query({ prompt: input, options });
2558
+ const abortController = options.abortController;
2559
+ const session = this.createSession(
2560
+ config.internalSessionId,
2561
+ q,
2562
+ input,
2563
+ config.permissionMode,
2564
+ config.cwd,
2565
+ abortController
2566
+ );
2567
+ return { query: q, input, session };
2568
+ }
2569
+ createCanUseTool(sessionId) {
2570
+ return async (toolName, toolInput, { suggestions, toolUseID }) => canUseTool({
2571
+ session: this.session,
2572
+ toolName,
2573
+ toolInput,
2574
+ toolUseID,
2575
+ suggestions,
2576
+ client: this.client,
2577
+ sessionId,
2578
+ fileContentCache: this.fileContentCache,
2579
+ logger: this.logger,
2580
+ emitConfigOptionsUpdate: () => this.emitConfigOptionsUpdate(sessionId)
2581
+ });
2582
+ }
2583
+ createOnModeChange(sessionId) {
2584
+ return async (newMode) => {
2585
+ if (this.session) {
2586
+ this.session.permissionMode = newMode;
2587
+ }
2588
+ await this.emitConfigOptionsUpdate(sessionId);
2589
+ };
2590
+ }
2591
+ async buildConfigOptions(modelOptionsOverride) {
2592
+ const options = [];
2593
+ const modeOptions = getAvailableModes().map((mode) => ({
2594
+ value: mode.id,
2595
+ name: mode.name,
2596
+ description: mode.description ?? void 0
2597
+ }));
2598
+ options.push({
2599
+ id: "mode",
2600
+ name: "Approval Preset",
2601
+ type: "select",
2602
+ currentValue: this.session.permissionMode,
2603
+ options: modeOptions,
2604
+ category: "mode",
2605
+ description: "Choose an approval and sandboxing preset for your session"
2606
+ });
2607
+ const modelOptions = modelOptionsOverride ?? await this.getModelConfigOptions(this.session.modelId);
2608
+ this.session.modelId = modelOptions.currentModelId;
2609
+ options.push({
2610
+ id: "model",
2611
+ name: "Model",
2612
+ type: "select",
2613
+ currentValue: modelOptions.currentModelId,
2614
+ options: modelOptions.options,
2615
+ category: "model",
2616
+ description: "Choose which model Claude should use"
2617
+ });
2618
+ return options;
2619
+ }
2620
+ async emitConfigOptionsUpdate(sessionId) {
2621
+ const configOptions = await this.buildConfigOptions();
2622
+ const serialized = JSON.stringify(configOptions);
2623
+ if (this.lastSentConfigOptions && JSON.stringify(this.lastSentConfigOptions) === serialized) {
2624
+ return;
2625
+ }
2626
+ this.lastSentConfigOptions = configOptions;
2627
+ await this.client.sessionUpdate({
2628
+ sessionId: sessionId ?? this.sessionId,
2629
+ update: {
2630
+ sessionUpdate: "config_option_update",
2631
+ configOptions
2632
+ }
2633
+ });
2634
+ }
2635
+ checkAuthStatus() {
2636
+ const backupExists = fs2.existsSync(
2637
+ path3.resolve(os3.homedir(), ".claude.json.backup")
2638
+ );
2639
+ const configExists = fs2.existsSync(
2640
+ path3.resolve(os3.homedir(), ".claude.json")
2641
+ );
2642
+ if (backupExists && !configExists) {
2643
+ throw RequestError2.authRequired();
2644
+ }
2645
+ }
2646
+ async trySetModel(q, modelId) {
2647
+ try {
2648
+ await this.setModelWithFallback(q, modelId);
2649
+ } catch (err) {
2650
+ this.logger.warn("Failed to set model", { modelId, error: err });
2651
+ }
2652
+ }
2653
+ async setModelWithFallback(q, modelId) {
2654
+ try {
2655
+ await q.setModel(modelId);
2656
+ return;
2657
+ } catch (err) {
2658
+ const fallback = toSdkModelId(modelId);
2659
+ if (fallback === modelId) {
2660
+ throw err;
2661
+ }
2662
+ await q.setModel(fallback);
2663
+ }
2664
+ }
2665
+ registerPersistence(sessionId, meta) {
2666
+ const persistence = meta?.persistence;
2667
+ if (persistence && this.logWriter) {
2668
+ this.logWriter.register(sessionId, persistence);
2669
+ }
2670
+ }
2671
+ sendAvailableCommandsUpdate(sessionId, availableCommands) {
2672
+ setTimeout(() => {
2673
+ this.client.sessionUpdate({
2674
+ sessionId,
2675
+ update: {
2676
+ sessionUpdate: "available_commands_update",
2677
+ availableCommands
2678
+ }
2679
+ });
2680
+ }, 0);
2681
+ }
2682
+ async broadcastUserMessage(params) {
2683
+ for (const chunk of params.prompt) {
2684
+ const notification = {
2685
+ sessionId: params.sessionId,
2686
+ update: {
2687
+ sessionUpdate: "user_message_chunk",
2688
+ content: chunk
2689
+ }
2690
+ };
2691
+ await this.client.sessionUpdate(notification);
2692
+ this.appendNotification(params.sessionId, notification);
2693
+ }
2694
+ }
2695
+ async processMessages(sessionId) {
2696
+ const context = {
2697
+ session: this.session,
2698
+ sessionId,
2699
+ client: this.client,
2700
+ toolUseCache: this.toolUseCache,
2701
+ fileContentCache: this.fileContentCache,
2702
+ logger: this.logger
2703
+ };
2704
+ while (true) {
2705
+ const { value: message, done } = await this.session.query.next();
2706
+ if (done || !message) {
2707
+ return this.handleSessionEnd();
2708
+ }
2709
+ const response = await this.handleMessage(message, context);
2710
+ if (response) {
2711
+ return response;
2712
+ }
2713
+ }
2714
+ }
2715
+ handleSessionEnd() {
2716
+ if (this.session.cancelled) {
2717
+ return {
2718
+ stopReason: "cancelled",
2719
+ _meta: this.session.interruptReason ? { interruptReason: this.session.interruptReason } : void 0
2720
+ };
2721
+ }
2722
+ throw new Error("Session did not end in result");
2723
+ }
2724
+ async handleMessage(message, context) {
2725
+ switch (message.type) {
2726
+ case "system":
2727
+ await handleSystemMessage(message, context);
2728
+ return null;
2729
+ case "result": {
2730
+ const result = handleResultMessage(message, context);
2731
+ if (result.error) throw result.error;
2732
+ if (result.shouldStop) {
2733
+ return {
2734
+ stopReason: result.stopReason
2735
+ };
2736
+ }
2737
+ return null;
2738
+ }
2739
+ case "stream_event":
2740
+ await handleStreamEvent(message, context);
2741
+ return null;
2742
+ case "user":
2743
+ case "assistant": {
2744
+ const result = await handleUserAssistantMessage(message, context);
2745
+ if (result.error) throw result.error;
2746
+ if (result.shouldStop) {
2747
+ return { stopReason: "end_turn" };
2748
+ }
2749
+ return null;
2750
+ }
2751
+ case "tool_progress":
2752
+ case "auth_status":
2753
+ return null;
2754
+ default:
2755
+ unreachable(message, this.logger);
2756
+ return null;
2757
+ }
2758
+ }
2759
+ };
2760
+
2761
+ // src/adapters/codex/spawn.ts
2762
+ import { spawn as spawn2 } from "child_process";
2763
+ import { existsSync as existsSync3 } from "fs";
2764
+ function buildConfigArgs(options) {
2765
+ const args = [];
2766
+ args.push("-c", `features.remote_models=false`);
2767
+ if (options.apiBaseUrl) {
2768
+ args.push("-c", `model_provider="posthog"`);
2769
+ args.push("-c", `model_providers.posthog.name="PostHog Gateway"`);
2770
+ args.push("-c", `model_providers.posthog.base_url="${options.apiBaseUrl}"`);
2771
+ args.push("-c", `model_providers.posthog.wire_api="responses"`);
2772
+ args.push(
2773
+ "-c",
2774
+ `model_providers.posthog.env_key="POSTHOG_GATEWAY_API_KEY"`
2775
+ );
2776
+ }
2777
+ if (options.model) {
2778
+ args.push("-c", `model="${options.model}"`);
2779
+ }
2780
+ return args;
2781
+ }
2782
+ function findCodexBinary(options) {
2783
+ const configArgs = buildConfigArgs(options);
2784
+ if (options.binaryPath && existsSync3(options.binaryPath)) {
2785
+ return { command: options.binaryPath, args: configArgs };
2786
+ }
2787
+ return { command: "npx", args: ["@zed-industries/codex-acp", ...configArgs] };
2788
+ }
2789
+ function spawnCodexProcess(options) {
2790
+ const logger = options.logger ?? new Logger({ debug: true, prefix: "[CodexSpawn]" });
2791
+ const env = { ...process.env };
2792
+ delete env.ELECTRON_RUN_AS_NODE;
2793
+ delete env.ELECTRON_NO_ASAR;
2794
+ if (options.apiKey) {
2795
+ env.POSTHOG_GATEWAY_API_KEY = options.apiKey;
2796
+ }
2797
+ const { command, args } = findCodexBinary(options);
2798
+ logger.info("Spawning codex-acp process", {
2799
+ command,
2800
+ args,
2801
+ cwd: options.cwd,
2802
+ hasApiBaseUrl: !!options.apiBaseUrl,
2803
+ hasApiKey: !!options.apiKey,
2804
+ binaryPath: options.binaryPath
2805
+ });
2806
+ const child = spawn2(command, args, {
2807
+ cwd: options.cwd,
2808
+ env,
2809
+ stdio: ["pipe", "pipe", "pipe"],
2810
+ detached: process.platform !== "win32"
2811
+ });
2812
+ child.stderr?.on("data", (data) => {
2813
+ logger.debug("codex-acp stderr:", data.toString());
2814
+ });
2815
+ child.on("error", (err) => {
2816
+ logger.error("codex-acp process error:", err);
2817
+ });
2818
+ child.on("exit", (code, signal) => {
2819
+ logger.info("codex-acp process exited", { code, signal });
2820
+ if (child.pid && options.processCallbacks?.onProcessExited) {
2821
+ options.processCallbacks.onProcessExited(child.pid);
2822
+ }
2823
+ });
2824
+ if (!child.stdin || !child.stdout) {
2825
+ throw new Error("Failed to get stdio streams from codex-acp process");
2826
+ }
2827
+ if (child.pid && options.processCallbacks?.onProcessSpawned) {
2828
+ options.processCallbacks.onProcessSpawned({
2829
+ pid: child.pid,
2830
+ command
2831
+ });
2832
+ }
2833
+ return {
2834
+ process: child,
2835
+ stdin: child.stdin,
2836
+ stdout: child.stdout,
2837
+ kill: () => {
2838
+ logger.info("Killing codex-acp process", { pid: child.pid });
2839
+ child.stdin?.destroy();
2840
+ child.stdout?.destroy();
2841
+ child.stderr?.destroy();
2842
+ child.kill("SIGTERM");
2843
+ }
2844
+ };
2845
+ }
2846
+
2847
+ // src/adapters/acp-connection.ts
2848
+ function isGroupedOptions(options) {
2849
+ return options.length > 0 && "group" in options[0];
2850
+ }
2851
+ function filterModelConfigOptions(msg, allowedModelIds) {
2852
+ const payload = msg;
2853
+ const configOptions = payload.result?.configOptions ?? payload.params?.update?.configOptions;
2854
+ if (!configOptions) return null;
2855
+ const filtered = configOptions.map((opt) => {
2856
+ if (opt.category !== "model" || !opt.options) return opt;
2857
+ const options = opt.options;
2858
+ if (isGroupedOptions(options)) {
2859
+ const filteredOptions2 = options.map((group) => ({
2860
+ ...group,
2861
+ options: (group.options ?? []).filter(
2862
+ (o) => o?.value && allowedModelIds.has(o.value)
2863
+ )
2864
+ }));
2865
+ const flat = filteredOptions2.flatMap((g) => g.options ?? []);
2866
+ const currentAllowed2 = opt.currentValue && allowedModelIds.has(opt.currentValue);
2867
+ const nextCurrent2 = currentAllowed2 || flat.length === 0 ? opt.currentValue : flat[0]?.value;
2868
+ return {
2869
+ ...opt,
2870
+ currentValue: nextCurrent2,
2871
+ options: filteredOptions2
2872
+ };
2873
+ }
2874
+ const valueOptions = options;
2875
+ const filteredOptions = valueOptions.filter(
2876
+ (o) => o?.value && allowedModelIds.has(o.value)
2877
+ );
2878
+ const currentAllowed = opt.currentValue && allowedModelIds.has(opt.currentValue);
2879
+ const nextCurrent = currentAllowed || filteredOptions.length === 0 ? opt.currentValue : filteredOptions[0]?.value;
2880
+ return {
2881
+ ...opt,
2882
+ currentValue: nextCurrent,
2883
+ options: filteredOptions
2884
+ };
2885
+ });
2886
+ if (payload.result?.configOptions) {
2887
+ return { ...msg, result: { ...payload.result, configOptions: filtered } };
2888
+ }
2889
+ if (payload.params?.update?.configOptions) {
2890
+ return {
2891
+ ...msg,
2892
+ params: {
2893
+ ...payload.params,
2894
+ update: { ...payload.params.update, configOptions: filtered }
2895
+ }
2896
+ };
2897
+ }
2898
+ return null;
2899
+ }
2900
+ function extractReasoningEffort(configOptions) {
2901
+ if (!configOptions) return void 0;
2902
+ const option = configOptions.find((opt) => opt.id === "reasoning_effort");
2903
+ return option?.currentValue ?? void 0;
2904
+ }
2905
+ function createAcpConnection(config = {}) {
2906
+ const adapterType = config.adapter ?? "claude";
2907
+ if (adapterType === "codex") {
2908
+ return createCodexConnection(config);
2909
+ }
2910
+ return createClaudeConnection(config);
2911
+ }
2912
+ function createClaudeConnection(config) {
2913
+ const logger = config.logger?.child("AcpConnection") ?? new Logger({ debug: true, prefix: "[AcpConnection]" });
2914
+ const streams = createBidirectionalStreams();
2915
+ const { logWriter } = config;
2916
+ let agentWritable = streams.agent.writable;
2917
+ let clientWritable = streams.client.writable;
2918
+ if (config.taskRunId && logWriter) {
2919
+ if (!logWriter.isRegistered(config.taskRunId)) {
2920
+ logWriter.register(config.taskRunId, {
2921
+ taskId: config.taskId ?? config.taskRunId,
2922
+ runId: config.taskRunId,
2923
+ deviceType: config.deviceType
2924
+ });
2925
+ }
2926
+ agentWritable = createTappedWritableStream(streams.agent.writable, {
2927
+ onMessage: (line) => {
2928
+ logWriter.appendRawLine(config.taskRunId, line);
2929
+ },
2930
+ logger
2931
+ });
2932
+ clientWritable = createTappedWritableStream(streams.client.writable, {
2933
+ onMessage: (line) => {
2934
+ logWriter.appendRawLine(config.taskRunId, line);
2935
+ },
2936
+ logger
2937
+ });
2938
+ } else {
2939
+ logger.info("Tapped streams NOT enabled", {
2940
+ hasTaskRunId: !!config.taskRunId,
2941
+ hasLogWriter: !!logWriter
2942
+ });
2943
+ }
2944
+ const agentStream = ndJsonStream(agentWritable, streams.agent.readable);
2945
+ let agent = null;
2946
+ const agentConnection = new AgentSideConnection((client) => {
2947
+ agent = new ClaudeAcpAgent(client, logWriter, config.processCallbacks);
2948
+ logger.info(`Created ${agent.adapterName} agent`);
2949
+ return agent;
2950
+ }, agentStream);
2951
+ return {
2952
+ agentConnection,
2953
+ clientStreams: {
2954
+ readable: streams.client.readable,
2955
+ writable: clientWritable
2956
+ },
2957
+ cleanup: async () => {
2958
+ logger.info("Cleaning up ACP connection");
2959
+ if (agent) {
2960
+ await agent.closeSession();
2961
+ }
2962
+ try {
2963
+ await streams.client.writable.close();
2964
+ } catch {
2965
+ }
2966
+ try {
2967
+ await streams.agent.writable.close();
2968
+ } catch {
2969
+ }
2970
+ }
2971
+ };
2972
+ }
2973
+ function createCodexConnection(config) {
2974
+ const logger = config.logger?.child("CodexConnection") ?? new Logger({ debug: true, prefix: "[CodexConnection]" });
2975
+ const { logWriter } = config;
2976
+ const allowedModelIds = config.allowedModelIds;
2977
+ const codexProcess = spawnCodexProcess({
2978
+ ...config.codexOptions,
2979
+ logger,
2980
+ processCallbacks: config.processCallbacks
2981
+ });
2982
+ let clientReadable = nodeReadableToWebReadable(codexProcess.stdout);
2983
+ let clientWritable = nodeWritableToWebWritable(codexProcess.stdin);
2984
+ let isLoadingSession = false;
2985
+ let loadRequestId = null;
2986
+ let newSessionRequestId = null;
2987
+ let sdkSessionEmitted = false;
2988
+ const reasoningEffortBySessionId = /* @__PURE__ */ new Map();
2989
+ let injectedConfigId = 0;
2990
+ const decoder = new TextDecoder();
2991
+ const encoder = new TextEncoder();
2992
+ let readBuffer = "";
2993
+ const taskRunId = config.taskRunId;
2994
+ const filteringReadable = clientReadable.pipeThrough(
2995
+ new TransformStream({
2996
+ transform(chunk, controller) {
2997
+ readBuffer += decoder.decode(chunk, { stream: true });
2998
+ const lines = readBuffer.split("\n");
2999
+ readBuffer = lines.pop() ?? "";
3000
+ const outputLines = [];
3001
+ for (const line of lines) {
3002
+ const trimmed = line.trim();
3003
+ if (!trimmed) {
3004
+ outputLines.push(line);
3005
+ continue;
3006
+ }
3007
+ let shouldFilter = false;
3008
+ try {
3009
+ const msg = JSON.parse(trimmed);
3010
+ const sessionId = msg?.params?.sessionId ?? msg?.result?.sessionId ?? null;
3011
+ const configOptions = msg?.result?.configOptions ?? msg?.params?.update?.configOptions;
3012
+ if (sessionId && configOptions) {
3013
+ const effort = extractReasoningEffort(configOptions);
3014
+ if (effort) {
3015
+ reasoningEffortBySessionId.set(sessionId, effort);
3016
+ }
3017
+ }
3018
+ if (!sdkSessionEmitted && newSessionRequestId !== null && msg.id === newSessionRequestId && "result" in msg) {
3019
+ const sessionId2 = msg.result?.sessionId;
3020
+ if (sessionId2 && taskRunId) {
3021
+ const sdkSessionNotification = {
3022
+ jsonrpc: "2.0",
3023
+ method: POSTHOG_NOTIFICATIONS.SDK_SESSION,
3024
+ params: {
3025
+ taskRunId,
3026
+ sessionId: sessionId2,
3027
+ adapter: "codex"
3028
+ }
3029
+ };
3030
+ outputLines.push(JSON.stringify(sdkSessionNotification));
3031
+ sdkSessionEmitted = true;
3032
+ }
3033
+ newSessionRequestId = null;
3034
+ }
3035
+ if (isLoadingSession) {
3036
+ if (msg.id === loadRequestId && "result" in msg) {
3037
+ logger.debug("session/load complete, resuming stream");
3038
+ isLoadingSession = false;
3039
+ loadRequestId = null;
3040
+ } else if (msg.method === "session/update") {
3041
+ shouldFilter = true;
3042
+ }
3043
+ }
3044
+ if (!shouldFilter && allowedModelIds && allowedModelIds.size > 0) {
3045
+ const updated = filterModelConfigOptions(msg, allowedModelIds);
3046
+ if (updated) {
3047
+ outputLines.push(JSON.stringify(updated));
3048
+ continue;
3049
+ }
3050
+ }
3051
+ } catch {
3052
+ }
3053
+ if (!shouldFilter) {
3054
+ outputLines.push(line);
3055
+ const isChunkNoise = trimmed.includes('"sessionUpdate":"agent_message_chunk"') || trimmed.includes('"sessionUpdate":"agent_thought_chunk"');
3056
+ if (!isChunkNoise) {
3057
+ logger.debug("codex-acp stdout:", trimmed);
3058
+ }
3059
+ }
3060
+ }
3061
+ if (outputLines.length > 0) {
3062
+ const output = `${outputLines.join("\n")}
3063
+ `;
3064
+ controller.enqueue(encoder.encode(output));
3065
+ }
3066
+ },
3067
+ flush(controller) {
3068
+ if (readBuffer.trim()) {
3069
+ controller.enqueue(encoder.encode(readBuffer));
3070
+ }
3071
+ }
3072
+ })
3073
+ );
3074
+ clientReadable = filteringReadable;
3075
+ const originalWritable = clientWritable;
3076
+ clientWritable = new WritableStream({
3077
+ write(chunk) {
3078
+ const text2 = decoder.decode(chunk, { stream: true });
3079
+ const trimmed = text2.trim();
3080
+ logger.debug("codex-acp stdin:", trimmed);
3081
+ try {
3082
+ const msg = JSON.parse(trimmed);
3083
+ if (msg.method === "session/set_config_option" && msg.params?.configId === "reasoning_effort" && msg.params?.sessionId && msg.params?.value) {
3084
+ reasoningEffortBySessionId.set(
3085
+ msg.params.sessionId,
3086
+ msg.params.value
3087
+ );
3088
+ }
3089
+ if (msg.method === "session/prompt" && msg.params?.sessionId) {
3090
+ const effort = reasoningEffortBySessionId.get(msg.params.sessionId);
3091
+ if (effort) {
3092
+ const injection = {
3093
+ jsonrpc: "2.0",
3094
+ id: `reasoning_effort_${Date.now()}_${injectedConfigId++}`,
3095
+ method: "session/set_config_option",
3096
+ params: {
3097
+ sessionId: msg.params.sessionId,
3098
+ configId: "reasoning_effort",
3099
+ value: effort
3100
+ }
3101
+ };
3102
+ const injectionLine = `${JSON.stringify(injection)}
3103
+ `;
3104
+ const writer2 = originalWritable.getWriter();
3105
+ return writer2.write(encoder.encode(injectionLine)).then(() => writer2.releaseLock()).then(() => {
3106
+ const nextWriter = originalWritable.getWriter();
3107
+ return nextWriter.write(chunk).finally(() => nextWriter.releaseLock());
3108
+ });
3109
+ }
3110
+ }
3111
+ if (msg.method === "session/new" && msg.id) {
3112
+ logger.debug("session/new detected, tracking request ID");
3113
+ newSessionRequestId = msg.id;
3114
+ } else if (msg.method === "session/load" && msg.id) {
3115
+ logger.debug("session/load detected, pausing stream updates");
3116
+ isLoadingSession = true;
3117
+ loadRequestId = msg.id;
3118
+ }
3119
+ } catch {
3120
+ }
3121
+ const writer = originalWritable.getWriter();
3122
+ return writer.write(chunk).finally(() => writer.releaseLock());
3123
+ },
3124
+ close() {
3125
+ const writer = originalWritable.getWriter();
3126
+ return writer.close().finally(() => writer.releaseLock());
3127
+ }
3128
+ });
3129
+ const shouldTapLogs = config.taskRunId && logWriter;
3130
+ if (shouldTapLogs) {
3131
+ const taskRunId2 = config.taskRunId;
3132
+ if (!logWriter.isRegistered(taskRunId2)) {
3133
+ logWriter.register(taskRunId2, {
3134
+ taskId: config.taskId ?? taskRunId2,
3135
+ runId: taskRunId2
3136
+ });
3137
+ }
3138
+ clientWritable = createTappedWritableStream(clientWritable, {
3139
+ onMessage: (line) => {
3140
+ logWriter.appendRawLine(taskRunId2, line);
3141
+ },
3142
+ logger
3143
+ });
3144
+ const originalReadable = clientReadable;
3145
+ const logDecoder = new TextDecoder();
3146
+ let logBuffer = "";
3147
+ clientReadable = originalReadable.pipeThrough(
3148
+ new TransformStream({
3149
+ transform(chunk, controller) {
3150
+ logBuffer += logDecoder.decode(chunk, { stream: true });
3151
+ const lines = logBuffer.split("\n");
3152
+ logBuffer = lines.pop() ?? "";
3153
+ for (const line of lines) {
3154
+ if (line.trim()) {
3155
+ logWriter.appendRawLine(taskRunId2, line);
3156
+ }
3157
+ }
3158
+ controller.enqueue(chunk);
3159
+ },
3160
+ flush() {
3161
+ if (logBuffer.trim()) {
3162
+ logWriter.appendRawLine(taskRunId2, logBuffer);
3163
+ }
3164
+ }
3165
+ })
3166
+ );
3167
+ } else {
3168
+ logger.info("Tapped streams NOT enabled for Codex", {
3169
+ hasTaskRunId: !!config.taskRunId,
3170
+ hasLogWriter: !!logWriter
3171
+ });
3172
+ }
3173
+ return {
3174
+ agentConnection: void 0,
3175
+ clientStreams: {
3176
+ readable: clientReadable,
3177
+ writable: clientWritable
3178
+ },
3179
+ cleanup: async () => {
3180
+ logger.info("Cleaning up Codex connection");
3181
+ codexProcess.kill();
3182
+ try {
3183
+ await clientWritable.close();
3184
+ } catch {
3185
+ }
3186
+ }
3187
+ };
3188
+ }
3189
+
3190
+ // src/utils/gateway.ts
3191
+ function getLlmGatewayUrl(posthogHost) {
3192
+ const url = new URL(posthogHost);
3193
+ const hostname = url.hostname;
3194
+ if (hostname === "localhost" || hostname === "127.0.0.1") {
3195
+ return `${url.protocol}//localhost:3308/twig`;
3196
+ }
3197
+ if (hostname === "host.docker.internal") {
3198
+ return `${url.protocol}//host.docker.internal:3308/twig`;
3199
+ }
3200
+ const region = hostname.match(/^(us|eu)\.posthog\.com$/)?.[1] ?? "us";
3201
+ return `https://gateway.${region}.posthog.com/twig`;
3202
+ }
3203
+
3204
+ // src/posthog-api.ts
3205
+ var PostHogAPIClient = class {
3206
+ config;
3207
+ constructor(config) {
3208
+ this.config = config;
3209
+ }
3210
+ get baseUrl() {
3211
+ const host = this.config.apiUrl.endsWith("/") ? this.config.apiUrl.slice(0, -1) : this.config.apiUrl;
3212
+ return host;
3213
+ }
3214
+ get headers() {
3215
+ return {
3216
+ Authorization: `Bearer ${this.config.getApiKey()}`,
3217
+ "Content-Type": "application/json"
3218
+ };
3219
+ }
3220
+ async apiRequest(endpoint, options = {}) {
3221
+ const url = `${this.baseUrl}${endpoint}`;
3222
+ const response = await fetch(url, {
3223
+ ...options,
3224
+ headers: {
3225
+ ...this.headers,
3226
+ ...options.headers
3227
+ }
3228
+ });
3229
+ if (!response.ok) {
3230
+ let errorMessage;
3231
+ try {
3232
+ const errorResponse = await response.json();
3233
+ errorMessage = `Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`;
3234
+ } catch {
3235
+ errorMessage = `Failed request: [${response.status}] ${response.statusText}`;
3236
+ }
3237
+ throw new Error(errorMessage);
3238
+ }
3239
+ return response.json();
3240
+ }
3241
+ getTeamId() {
3242
+ return this.config.projectId;
3243
+ }
3244
+ getApiKey() {
3245
+ return this.config.getApiKey();
3246
+ }
3247
+ getLlmGatewayUrl() {
3248
+ return getLlmGatewayUrl(this.baseUrl);
3249
+ }
3250
+ async getTask(taskId) {
3251
+ const teamId = this.getTeamId();
3252
+ return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/`);
3253
+ }
3254
+ async getTaskRun(taskId, runId) {
3255
+ const teamId = this.getTeamId();
3256
+ return this.apiRequest(
3257
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`
3258
+ );
3259
+ }
3260
+ async updateTaskRun(taskId, runId, payload) {
3261
+ const teamId = this.getTeamId();
3262
+ return this.apiRequest(
3263
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`,
3264
+ {
3265
+ method: "PATCH",
3266
+ body: JSON.stringify(payload)
3267
+ }
3268
+ );
3269
+ }
3270
+ async appendTaskRunLog(taskId, runId, entries) {
3271
+ const teamId = this.getTeamId();
3272
+ return this.apiRequest(
3273
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/append_log/`,
3274
+ {
3275
+ method: "POST",
3276
+ body: JSON.stringify({ entries })
3277
+ }
3278
+ );
3279
+ }
3280
+ async uploadTaskArtifacts(taskId, runId, artifacts) {
3281
+ if (!artifacts.length) {
3282
+ return [];
3283
+ }
3284
+ const teamId = this.getTeamId();
3285
+ const response = await this.apiRequest(
3286
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/`,
3287
+ {
3288
+ method: "POST",
3289
+ body: JSON.stringify({ artifacts })
3290
+ }
3291
+ );
3292
+ return response.artifacts ?? [];
3293
+ }
3294
+ async getArtifactPresignedUrl(taskId, runId, storagePath) {
3295
+ const teamId = this.getTeamId();
3296
+ try {
3297
+ const response = await this.apiRequest(
3298
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/presign/`,
3299
+ {
3300
+ method: "POST",
3301
+ body: JSON.stringify({ storage_path: storagePath })
3302
+ }
3303
+ );
3304
+ return response.url;
3305
+ } catch {
3306
+ return null;
3307
+ }
3308
+ }
3309
+ /**
3310
+ * Download artifact content by storage path
3311
+ * Gets a presigned URL and fetches the content
3312
+ */
3313
+ async downloadArtifact(taskId, runId, storagePath) {
3314
+ const url = await this.getArtifactPresignedUrl(taskId, runId, storagePath);
3315
+ if (!url) {
3316
+ return null;
3317
+ }
3318
+ try {
3319
+ const response = await fetch(url);
3320
+ if (!response.ok) {
3321
+ throw new Error(`Failed to download artifact: ${response.status}`);
3322
+ }
3323
+ return response.arrayBuffer();
3324
+ } catch {
3325
+ return null;
3326
+ }
3327
+ }
3328
+ /**
3329
+ * Fetch logs for a task run via the logs API endpoint
3330
+ * @param taskRun - The task run to fetch logs for
3331
+ * @returns Array of stored entries, or empty array if no logs available
3332
+ */
3333
+ async fetchTaskRunLogs(taskRun) {
3334
+ const teamId = this.getTeamId();
3335
+ try {
3336
+ const response = await fetch(
3337
+ `${this.baseUrl}/api/projects/${teamId}/tasks/${taskRun.task}/runs/${taskRun.id}/logs`,
3338
+ { headers: this.headers }
3339
+ );
3340
+ if (!response.ok) {
3341
+ if (response.status === 404) {
3342
+ return [];
3343
+ }
3344
+ throw new Error(
3345
+ `Failed to fetch logs: ${response.status} ${response.statusText}`
3346
+ );
3347
+ }
3348
+ const content = await response.text();
3349
+ if (!content.trim()) {
3350
+ return [];
3351
+ }
3352
+ return content.trim().split("\n").map((line) => JSON.parse(line));
3353
+ } catch (error) {
3354
+ throw new Error(
3355
+ `Failed to fetch task run logs: ${error instanceof Error ? error.message : String(error)}`
3356
+ );
3357
+ }
3358
+ }
3359
+ };
3360
+
3361
+ // src/otel-log-writer.ts
3362
+ import { SeverityNumber } from "@opentelemetry/api-logs";
3363
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
3364
+ import { resourceFromAttributes } from "@opentelemetry/resources";
3365
+ import {
3366
+ BatchLogRecordProcessor,
3367
+ LoggerProvider
3368
+ } from "@opentelemetry/sdk-logs";
3369
+ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
3370
+ var OtelLogWriter = class {
3371
+ loggerProvider;
3372
+ logger;
3373
+ constructor(config, sessionContext, _debugLogger) {
3374
+ const logsPath = config.logsPath ?? "/i/v1/agent-logs";
3375
+ const exporter = new OTLPLogExporter({
3376
+ url: `${config.posthogHost}${logsPath}`,
3377
+ headers: { Authorization: `Bearer ${config.apiKey}` }
3378
+ });
3379
+ const processor = new BatchLogRecordProcessor(exporter, {
3380
+ scheduledDelayMillis: config.flushIntervalMs ?? 500
3381
+ });
3382
+ this.loggerProvider = new LoggerProvider({
3383
+ resource: resourceFromAttributes({
3384
+ [ATTR_SERVICE_NAME]: "twig-agent",
3385
+ run_id: sessionContext.runId,
3386
+ task_id: sessionContext.taskId,
3387
+ device_type: sessionContext.deviceType ?? "local"
3388
+ }),
3389
+ processors: [processor]
3390
+ });
3391
+ this.logger = this.loggerProvider.getLogger("agent-session");
3392
+ }
3393
+ /**
3394
+ * Emit an agent event to PostHog Logs via OTEL.
3395
+ */
3396
+ emit(entry) {
3397
+ const { notification } = entry;
3398
+ const eventType = notification.notification.method;
3399
+ this.logger.emit({
3400
+ severityNumber: SeverityNumber.INFO,
3401
+ severityText: "INFO",
3402
+ body: JSON.stringify(notification),
3403
+ attributes: {
3404
+ event_type: eventType
3405
+ }
3406
+ });
3407
+ }
3408
+ async flush() {
3409
+ await this.loggerProvider.forceFlush();
3410
+ }
3411
+ async shutdown() {
3412
+ await this.loggerProvider.shutdown();
3413
+ }
3414
+ };
3415
+
3416
+ // src/session-log-writer.ts
3417
+ var SessionLogWriter = class {
3418
+ posthogAPI;
3419
+ otelConfig;
3420
+ pendingEntries = /* @__PURE__ */ new Map();
3421
+ flushTimeouts = /* @__PURE__ */ new Map();
3422
+ sessions = /* @__PURE__ */ new Map();
3423
+ logger;
3424
+ constructor(options = {}) {
3425
+ this.posthogAPI = options.posthogAPI;
3426
+ this.otelConfig = options.otelConfig;
3427
+ this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
3428
+ }
3429
+ async flushAll() {
3430
+ const flushPromises = [];
3431
+ for (const sessionId of this.sessions.keys()) {
3432
+ flushPromises.push(this.flush(sessionId));
3433
+ }
3434
+ await Promise.all(flushPromises);
3435
+ }
3436
+ register(sessionId, context) {
3437
+ if (this.sessions.has(sessionId)) {
3438
+ return;
3439
+ }
3440
+ let otelWriter;
3441
+ if (this.otelConfig) {
3442
+ otelWriter = new OtelLogWriter(
3443
+ this.otelConfig,
3444
+ context,
3445
+ this.logger.child(`OtelWriter:${sessionId}`)
3446
+ );
3447
+ }
3448
+ this.sessions.set(sessionId, { context, otelWriter });
3449
+ }
3450
+ isRegistered(sessionId) {
3451
+ return this.sessions.has(sessionId);
3452
+ }
3453
+ appendRawLine(sessionId, line) {
3454
+ const session = this.sessions.get(sessionId);
3455
+ if (!session) {
3456
+ return;
3457
+ }
3458
+ try {
3459
+ const message = JSON.parse(line);
3460
+ const entry = {
3461
+ type: "notification",
3462
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3463
+ notification: message
3464
+ };
3465
+ if (session.otelWriter) {
3466
+ session.otelWriter.emit({ notification: entry });
3467
+ }
3468
+ if (this.posthogAPI) {
3469
+ const pending = this.pendingEntries.get(sessionId) ?? [];
3470
+ pending.push(entry);
3471
+ this.pendingEntries.set(sessionId, pending);
3472
+ this.scheduleFlush(sessionId);
3473
+ }
3474
+ } catch {
3475
+ this.logger.warn("Failed to parse raw line for persistence", {
3476
+ sessionId,
3477
+ lineLength: line.length
3478
+ });
3479
+ }
3480
+ }
3481
+ async flush(sessionId) {
3482
+ const session = this.sessions.get(sessionId);
3483
+ if (!session) return;
3484
+ if (session.otelWriter) {
3485
+ await session.otelWriter.flush();
3486
+ }
3487
+ const pending = this.pendingEntries.get(sessionId);
3488
+ if (!this.posthogAPI || !pending?.length) return;
3489
+ this.pendingEntries.delete(sessionId);
3490
+ const timeout = this.flushTimeouts.get(sessionId);
3491
+ if (timeout) {
3492
+ clearTimeout(timeout);
3493
+ this.flushTimeouts.delete(sessionId);
3494
+ }
3495
+ try {
3496
+ await this.posthogAPI.appendTaskRunLog(
3497
+ session.context.taskId,
3498
+ session.context.runId,
3499
+ pending
3500
+ );
3501
+ } catch (error) {
3502
+ this.logger.error("Failed to persist session logs:", error);
3503
+ }
3504
+ }
3505
+ scheduleFlush(sessionId) {
3506
+ const existing = this.flushTimeouts.get(sessionId);
3507
+ if (existing) clearTimeout(existing);
3508
+ const timeout = setTimeout(() => this.flush(sessionId), 500);
3509
+ this.flushTimeouts.set(sessionId, timeout);
3510
+ }
3511
+ };
3512
+
3513
+ // src/tree-tracker.ts
3514
+ import { isCommitOnRemote as gitIsCommitOnRemote } from "@twig/git/queries";
3515
+
3516
+ // src/sagas/apply-snapshot-saga.ts
3517
+ import { mkdir, rm, writeFile } from "fs/promises";
3518
+ import { join as join3 } from "path";
3519
+ import { Saga } from "@posthog/shared";
3520
+ import { ApplyTreeSaga as GitApplyTreeSaga } from "@twig/git/sagas/tree";
3521
+ var ApplySnapshotSaga = class extends Saga {
3522
+ archivePath = null;
3523
+ async execute(input) {
3524
+ const { snapshot, repositoryPath, apiClient, taskId, runId } = input;
3525
+ const tmpDir = join3(repositoryPath, ".posthog", "tmp");
3526
+ if (!snapshot.archiveUrl) {
3527
+ throw new Error("Cannot apply snapshot: no archive URL");
3528
+ }
3529
+ await this.step({
3530
+ name: "create_tmp_dir",
3531
+ execute: () => mkdir(tmpDir, { recursive: true }),
3532
+ rollback: async () => {
3533
+ }
3534
+ });
3535
+ this.archivePath = join3(tmpDir, `${snapshot.treeHash}.tar.gz`);
3536
+ await this.step({
3537
+ name: "download_archive",
3538
+ execute: async () => {
3539
+ const arrayBuffer = await apiClient.downloadArtifact(
3540
+ taskId,
3541
+ runId,
3542
+ snapshot.archiveUrl
3543
+ );
3544
+ if (!arrayBuffer) {
3545
+ throw new Error("Failed to download archive");
3546
+ }
3547
+ const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
3548
+ const binaryContent = Buffer.from(base64Content, "base64");
3549
+ await writeFile(this.archivePath, binaryContent);
3550
+ },
3551
+ rollback: async () => {
3552
+ if (this.archivePath) {
3553
+ await rm(this.archivePath, { force: true }).catch(() => {
3554
+ });
3555
+ }
3556
+ }
3557
+ });
3558
+ const gitApplySaga = new GitApplyTreeSaga(this.log);
3559
+ const applyResult = await gitApplySaga.run({
3560
+ baseDir: repositoryPath,
3561
+ treeHash: snapshot.treeHash,
3562
+ baseCommit: snapshot.baseCommit,
3563
+ changes: snapshot.changes,
3564
+ archivePath: this.archivePath
3565
+ });
3566
+ if (!applyResult.success) {
3567
+ throw new Error(`Failed to apply tree: ${applyResult.error}`);
3568
+ }
3569
+ await rm(this.archivePath, { force: true }).catch(() => {
3570
+ });
3571
+ this.log.info("Tree snapshot applied", {
3572
+ treeHash: snapshot.treeHash,
3573
+ totalChanges: snapshot.changes.length,
3574
+ deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
3575
+ });
3576
+ return { treeHash: snapshot.treeHash };
3577
+ }
3578
+ };
3579
+
3580
+ // src/sagas/capture-tree-saga.ts
3581
+ import { existsSync as existsSync4 } from "fs";
3582
+ import { readFile, rm as rm2 } from "fs/promises";
3583
+ import { join as join4 } from "path";
3584
+ import { Saga as Saga2 } from "@posthog/shared";
3585
+ import { CaptureTreeSaga as GitCaptureTreeSaga } from "@twig/git/sagas/tree";
3586
+ var CaptureTreeSaga = class extends Saga2 {
3587
+ async execute(input) {
3588
+ const {
3589
+ repositoryPath,
3590
+ lastTreeHash,
3591
+ interrupted,
3592
+ apiClient,
3593
+ taskId,
3594
+ runId
3595
+ } = input;
3596
+ const tmpDir = join4(repositoryPath, ".posthog", "tmp");
3597
+ if (existsSync4(join4(repositoryPath, ".gitmodules"))) {
3598
+ this.log.warn(
3599
+ "Repository has submodules - snapshot may not capture submodule state"
3600
+ );
3601
+ }
3602
+ const shouldArchive = !!apiClient;
3603
+ const archivePath = shouldArchive ? join4(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
3604
+ const gitCaptureSaga = new GitCaptureTreeSaga(this.log);
3605
+ const captureResult = await gitCaptureSaga.run({
3606
+ baseDir: repositoryPath,
3607
+ lastTreeHash,
3608
+ archivePath
3609
+ });
3610
+ if (!captureResult.success) {
3611
+ throw new Error(`Failed to capture tree: ${captureResult.error}`);
3612
+ }
3613
+ const {
3614
+ snapshot: gitSnapshot,
3615
+ archivePath: createdArchivePath,
3616
+ changed
3617
+ } = captureResult.data;
3618
+ if (!changed || !gitSnapshot) {
3619
+ this.log.debug("No changes since last capture", { lastTreeHash });
3620
+ return { snapshot: null, newTreeHash: lastTreeHash };
3621
+ }
3622
+ let archiveUrl;
3623
+ if (apiClient && createdArchivePath) {
3624
+ try {
3625
+ archiveUrl = await this.uploadArchive(
3626
+ createdArchivePath,
3627
+ gitSnapshot.treeHash,
3628
+ apiClient,
3629
+ taskId,
3630
+ runId
3631
+ );
3632
+ } finally {
3633
+ await rm2(createdArchivePath, { force: true }).catch(() => {
3634
+ });
3635
+ }
3636
+ }
3637
+ const snapshot = {
3638
+ treeHash: gitSnapshot.treeHash,
3639
+ baseCommit: gitSnapshot.baseCommit,
3640
+ changes: gitSnapshot.changes,
3641
+ timestamp: gitSnapshot.timestamp,
3642
+ interrupted,
3643
+ archiveUrl
3644
+ };
3645
+ this.log.info("Tree captured", {
3646
+ treeHash: snapshot.treeHash,
3647
+ changes: snapshot.changes.length,
3648
+ interrupted,
3649
+ archiveUrl
3650
+ });
3651
+ return { snapshot, newTreeHash: snapshot.treeHash };
3652
+ }
3653
+ async uploadArchive(archivePath, treeHash, apiClient, taskId, runId) {
3654
+ const archiveUrl = await this.step({
3655
+ name: "upload_archive",
3656
+ execute: async () => {
3657
+ const archiveContent = await readFile(archivePath);
3658
+ const base64Content = archiveContent.toString("base64");
3659
+ const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
3660
+ {
3661
+ name: `trees/${treeHash}.tar.gz`,
3662
+ type: "tree_snapshot",
3663
+ content: base64Content,
3664
+ content_type: "application/gzip"
3665
+ }
3666
+ ]);
3667
+ if (artifacts.length > 0 && artifacts[0].storage_path) {
3668
+ this.log.info("Tree archive uploaded", {
3669
+ storagePath: artifacts[0].storage_path,
3670
+ treeHash
3671
+ });
3672
+ return artifacts[0].storage_path;
3673
+ }
3674
+ return void 0;
3675
+ },
3676
+ rollback: async () => {
3677
+ await rm2(archivePath, { force: true }).catch(() => {
3678
+ });
3679
+ }
3680
+ });
3681
+ return archiveUrl;
3682
+ }
3683
+ };
3684
+
3685
+ // src/tree-tracker.ts
3686
+ var TreeTracker = class {
3687
+ repositoryPath;
3688
+ taskId;
3689
+ runId;
3690
+ apiClient;
3691
+ logger;
3692
+ lastTreeHash = null;
3693
+ constructor(config) {
3694
+ this.repositoryPath = config.repositoryPath;
3695
+ this.taskId = config.taskId;
3696
+ this.runId = config.runId;
3697
+ this.apiClient = config.apiClient;
3698
+ this.logger = config.logger || new Logger({ debug: false, prefix: "[TreeTracker]" });
3699
+ }
3700
+ /**
3701
+ * Capture current working tree state as a snapshot.
3702
+ * Uses a temporary index to avoid modifying user's staging area.
3703
+ * Uses Saga pattern for atomic operation with automatic cleanup on failure.
3704
+ */
3705
+ async captureTree(options) {
3706
+ const saga = new CaptureTreeSaga(this.logger);
3707
+ const result = await saga.run({
3708
+ repositoryPath: this.repositoryPath,
3709
+ taskId: this.taskId,
3710
+ runId: this.runId,
3711
+ apiClient: this.apiClient,
3712
+ lastTreeHash: this.lastTreeHash,
3713
+ interrupted: options?.interrupted
3714
+ });
3715
+ if (!result.success) {
3716
+ this.logger.error("Failed to capture tree", {
3717
+ error: result.error,
3718
+ failedStep: result.failedStep
3719
+ });
3720
+ throw new Error(
3721
+ `Failed to capture tree at step '${result.failedStep}': ${result.error}`
3722
+ );
3723
+ }
3724
+ if (result.data.newTreeHash !== null) {
3725
+ this.lastTreeHash = result.data.newTreeHash;
3726
+ }
3727
+ return result.data.snapshot;
3728
+ }
3729
+ /**
3730
+ * Download and apply a tree snapshot.
3731
+ * Uses Saga pattern for atomic operation with rollback on failure.
3732
+ */
3733
+ async applyTreeSnapshot(snapshot) {
3734
+ if (!this.apiClient) {
3735
+ throw new Error("Cannot apply snapshot: API client not configured");
3736
+ }
3737
+ if (!snapshot.archiveUrl) {
3738
+ this.logger.warn("Cannot apply snapshot: no archive URL", {
3739
+ treeHash: snapshot.treeHash,
3740
+ changes: snapshot.changes.length
3741
+ });
3742
+ throw new Error("Cannot apply snapshot: no archive URL");
3743
+ }
3744
+ const saga = new ApplySnapshotSaga(this.logger);
3745
+ const result = await saga.run({
3746
+ snapshot,
3747
+ repositoryPath: this.repositoryPath,
3748
+ apiClient: this.apiClient,
3749
+ taskId: this.taskId,
3750
+ runId: this.runId
3751
+ });
3752
+ if (!result.success) {
3753
+ this.logger.error("Failed to apply tree snapshot", {
3754
+ error: result.error,
3755
+ failedStep: result.failedStep,
3756
+ treeHash: snapshot.treeHash
3757
+ });
3758
+ throw new Error(
3759
+ `Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
3760
+ );
3761
+ }
3762
+ this.lastTreeHash = result.data.treeHash;
3763
+ }
3764
+ /**
3765
+ * Get the last captured tree hash.
3766
+ */
3767
+ getLastTreeHash() {
3768
+ return this.lastTreeHash;
3769
+ }
3770
+ /**
3771
+ * Set the last tree hash (used when resuming).
3772
+ */
3773
+ setLastTreeHash(hash) {
3774
+ this.lastTreeHash = hash;
3775
+ }
3776
+ };
3777
+
3778
+ // src/utils/async-mutex.ts
3779
+ var AsyncMutex = class {
3780
+ locked = false;
3781
+ queue = [];
3782
+ async acquire() {
3783
+ if (!this.locked) {
3784
+ this.locked = true;
3785
+ return;
3786
+ }
3787
+ return new Promise((resolve3) => {
3788
+ this.queue.push(resolve3);
3789
+ });
3790
+ }
3791
+ release() {
3792
+ const next = this.queue.shift();
3793
+ if (next) {
3794
+ next();
3795
+ } else {
3796
+ this.locked = false;
3797
+ }
3798
+ }
3799
+ isLocked() {
3800
+ return this.locked;
3801
+ }
3802
+ get queueLength() {
3803
+ return this.queue.length;
3804
+ }
3805
+ };
3806
+
3807
+ // src/server/jwt.ts
3808
+ import jwt from "jsonwebtoken";
3809
+ import { z as z2 } from "zod";
3810
+ var SANDBOX_CONNECTION_AUDIENCE = "posthog:sandbox_connection";
3811
+ var userDataSchema = z2.object({
3812
+ run_id: z2.string(),
3813
+ task_id: z2.string(),
3814
+ team_id: z2.number(),
3815
+ user_id: z2.number(),
3816
+ distinct_id: z2.string(),
3817
+ mode: z2.enum(["interactive", "background"]).optional().default("interactive")
3818
+ });
3819
+ var jwtPayloadSchema = userDataSchema.extend({
3820
+ exp: z2.number(),
3821
+ iat: z2.number().optional(),
3822
+ aud: z2.string().optional()
3823
+ });
3824
+ var JwtValidationError = class extends Error {
3825
+ constructor(message, code) {
3826
+ super(message);
3827
+ this.code = code;
3828
+ this.name = "JwtValidationError";
3829
+ }
3830
+ };
3831
+ function validateJwt(token, publicKey) {
3832
+ try {
3833
+ const decoded = jwt.verify(token, publicKey, {
3834
+ algorithms: ["RS256"],
3835
+ audience: SANDBOX_CONNECTION_AUDIENCE
3836
+ });
3837
+ const result = jwtPayloadSchema.safeParse(decoded);
3838
+ if (!result.success) {
3839
+ throw new JwtValidationError(
3840
+ `Missing required fields: ${result.error.message}`,
3841
+ "invalid_token"
3842
+ );
3843
+ }
3844
+ return result.data;
3845
+ } catch (error) {
3846
+ if (error instanceof JwtValidationError) {
3847
+ throw error;
3848
+ }
3849
+ if (error instanceof jwt.TokenExpiredError) {
3850
+ throw new JwtValidationError("Token expired", "expired");
3851
+ }
3852
+ if (error instanceof jwt.JsonWebTokenError) {
3853
+ throw new JwtValidationError("Invalid signature", "invalid_signature");
3854
+ }
3855
+ throw new JwtValidationError("Invalid token", "invalid_token");
3856
+ }
3857
+ }
3858
+
3859
+ // src/server/schemas.ts
3860
+ import { z as z3 } from "zod";
3861
+ var jsonRpcRequestSchema = z3.object({
3862
+ jsonrpc: z3.literal("2.0"),
3863
+ method: z3.string(),
3864
+ params: z3.record(z3.unknown()).optional(),
3865
+ id: z3.union([z3.string(), z3.number()]).optional()
3866
+ });
3867
+ var userMessageParamsSchema = z3.object({
3868
+ content: z3.string().min(1, "Content is required")
3869
+ });
3870
+ var commandParamsSchemas = {
3871
+ user_message: userMessageParamsSchema,
3872
+ "posthog/user_message": userMessageParamsSchema,
3873
+ cancel: z3.object({}).optional(),
3874
+ "posthog/cancel": z3.object({}).optional(),
3875
+ close: z3.object({}).optional(),
3876
+ "posthog/close": z3.object({}).optional()
3877
+ };
3878
+ function validateCommandParams(method, params) {
3879
+ const schema = commandParamsSchemas[method] ?? commandParamsSchemas[method.replace("posthog/", "")];
3880
+ if (!schema) {
3881
+ return { success: false, error: `Unknown method: ${method}` };
3882
+ }
3883
+ const result = schema.safeParse(params);
3884
+ if (!result.success) {
3885
+ return { success: false, error: result.error.message };
3886
+ }
3887
+ return { success: true, data: result.data };
3888
+ }
3889
+
3890
+ // src/server/agent-server.ts
3891
+ var NdJsonTap = class {
3892
+ constructor(onMessage) {
3893
+ this.onMessage = onMessage;
3894
+ }
3895
+ decoder = new TextDecoder();
3896
+ buffer = "";
3897
+ process(chunk) {
3898
+ this.buffer += this.decoder.decode(chunk, { stream: true });
3899
+ const lines = this.buffer.split("\n");
3900
+ this.buffer = lines.pop() ?? "";
3901
+ for (const line of lines) {
3902
+ if (!line.trim()) continue;
3903
+ try {
3904
+ this.onMessage(JSON.parse(line));
3905
+ } catch {
3906
+ }
3907
+ }
3908
+ }
3909
+ };
3910
+ function createTappedReadableStream(underlying, onMessage, logger) {
3911
+ const reader = underlying.getReader();
3912
+ const tap = new NdJsonTap(onMessage);
3913
+ return new ReadableStream({
3914
+ async pull(controller) {
3915
+ try {
3916
+ const { value, done } = await reader.read();
3917
+ if (done) {
3918
+ controller.close();
3919
+ return;
3920
+ }
3921
+ tap.process(value);
3922
+ controller.enqueue(value);
3923
+ } catch (error) {
3924
+ logger?.debug("Read failed, closing stream", error);
3925
+ controller.close();
3926
+ }
3927
+ },
3928
+ cancel() {
3929
+ reader.releaseLock();
3930
+ }
3931
+ });
3932
+ }
3933
+ function createTappedWritableStream2(underlying, onMessage, logger) {
3934
+ const tap = new NdJsonTap(onMessage);
3935
+ const mutex = new AsyncMutex();
3936
+ return new WritableStream({
3937
+ async write(chunk) {
3938
+ tap.process(chunk);
3939
+ await mutex.acquire();
3940
+ try {
3941
+ const writer = underlying.getWriter();
3942
+ await writer.write(chunk);
3943
+ writer.releaseLock();
3944
+ } catch (error) {
3945
+ logger?.debug("Write failed (stream may be closed)", error);
3946
+ } finally {
3947
+ mutex.release();
3948
+ }
3949
+ },
3950
+ async close() {
3951
+ await mutex.acquire();
3952
+ try {
3953
+ const writer = underlying.getWriter();
3954
+ await writer.close();
3955
+ writer.releaseLock();
3956
+ } catch (error) {
3957
+ logger?.debug("Close failed (stream may be closed)", error);
3958
+ } finally {
3959
+ mutex.release();
3960
+ }
3961
+ },
3962
+ async abort(reason) {
3963
+ await mutex.acquire();
3964
+ try {
3965
+ const writer = underlying.getWriter();
3966
+ await writer.abort(reason);
3967
+ writer.releaseLock();
3968
+ } catch (error) {
3969
+ logger?.debug("Abort failed (stream may be closed)", error);
3970
+ } finally {
3971
+ mutex.release();
3972
+ }
3973
+ }
3974
+ });
3975
+ }
3976
+ var AgentServer = class {
3977
+ config;
3978
+ logger;
3979
+ server = null;
3980
+ session = null;
3981
+ app;
3982
+ posthogAPI;
3983
+ constructor(config) {
3984
+ this.config = config;
3985
+ this.logger = new Logger({ debug: true, prefix: "[AgentServer]" });
3986
+ this.posthogAPI = new PostHogAPIClient({
3987
+ apiUrl: config.apiUrl,
3988
+ projectId: config.projectId,
3989
+ getApiKey: () => config.apiKey
3990
+ });
3991
+ this.app = this.createApp();
3992
+ }
3993
+ getEffectiveMode(payload) {
3994
+ return payload.mode ?? this.config.mode;
3995
+ }
3996
+ createApp() {
3997
+ const app = new Hono();
3998
+ app.get("/health", (c) => {
3999
+ return c.json({ status: "ok", hasSession: !!this.session });
4000
+ });
4001
+ app.get("/events", async (c) => {
4002
+ let payload;
4003
+ try {
4004
+ payload = this.authenticateRequest(c.req.header.bind(c.req));
4005
+ } catch (error) {
4006
+ return c.json(
4007
+ {
4008
+ error: error instanceof JwtValidationError ? error.message : "Invalid token",
4009
+ code: error instanceof JwtValidationError ? error.code : "invalid_token"
4010
+ },
4011
+ 401
4012
+ );
4013
+ }
4014
+ const stream = new ReadableStream({
4015
+ start: async (controller) => {
4016
+ const sseController = {
4017
+ send: (data) => {
4018
+ try {
4019
+ controller.enqueue(
4020
+ new TextEncoder().encode(`data: ${JSON.stringify(data)}
4021
+
4022
+ `)
4023
+ );
4024
+ } catch (error) {
4025
+ this.logger.debug(
4026
+ "SSE send failed (stream may be closed)",
4027
+ error
4028
+ );
4029
+ }
4030
+ },
4031
+ close: () => {
4032
+ try {
4033
+ controller.close();
4034
+ } catch (error) {
4035
+ this.logger.debug("SSE close failed (already closed)", error);
4036
+ }
4037
+ }
4038
+ };
4039
+ if (!this.session || this.session.payload.run_id !== payload.run_id) {
4040
+ await this.initializeSession(payload, sseController);
4041
+ } else {
4042
+ this.session.sseController = sseController;
4043
+ }
4044
+ this.sendSseEvent(sseController, {
4045
+ type: "connected",
4046
+ run_id: payload.run_id
4047
+ });
4048
+ },
4049
+ cancel: () => {
4050
+ this.logger.info("SSE connection closed");
4051
+ if (this.session?.sseController) {
4052
+ this.session.sseController = null;
4053
+ }
4054
+ }
4055
+ });
4056
+ return new Response(stream, {
4057
+ headers: {
4058
+ "Content-Type": "text/event-stream",
4059
+ "Cache-Control": "no-cache",
4060
+ Connection: "keep-alive"
4061
+ }
4062
+ });
4063
+ });
4064
+ app.post("/command", async (c) => {
4065
+ let payload;
4066
+ try {
4067
+ payload = this.authenticateRequest(c.req.header.bind(c.req));
4068
+ } catch (error) {
4069
+ return c.json(
4070
+ {
4071
+ error: error instanceof JwtValidationError ? error.message : "Invalid token"
4072
+ },
4073
+ 401
4074
+ );
4075
+ }
4076
+ if (!this.session || this.session.payload.run_id !== payload.run_id) {
4077
+ return c.json({ error: "No active session for this run" }, 400);
4078
+ }
4079
+ const rawBody = await c.req.json().catch(() => null);
4080
+ const parseResult = jsonRpcRequestSchema.safeParse(rawBody);
4081
+ if (!parseResult.success) {
4082
+ return c.json({ error: "Invalid JSON-RPC request" }, 400);
4083
+ }
4084
+ const command = parseResult.data;
4085
+ const paramsValidation = validateCommandParams(
4086
+ command.method,
4087
+ command.params ?? {}
4088
+ );
4089
+ if (!paramsValidation.success) {
4090
+ return c.json(
4091
+ {
4092
+ jsonrpc: "2.0",
4093
+ id: command.id,
4094
+ error: {
4095
+ code: -32602,
4096
+ message: paramsValidation.error
4097
+ }
4098
+ },
4099
+ 200
4100
+ );
4101
+ }
4102
+ try {
4103
+ const result = await this.executeCommand(
4104
+ command.method,
4105
+ command.params || {}
4106
+ );
4107
+ return c.json({
4108
+ jsonrpc: "2.0",
4109
+ id: command.id,
4110
+ result
4111
+ });
4112
+ } catch (error) {
4113
+ return c.json({
4114
+ jsonrpc: "2.0",
4115
+ id: command.id,
4116
+ error: {
4117
+ code: -32e3,
4118
+ message: error instanceof Error ? error.message : "Unknown error"
4119
+ }
4120
+ });
4121
+ }
4122
+ });
4123
+ app.notFound((c) => {
4124
+ return c.json({ error: "Not found" }, 404);
4125
+ });
4126
+ return app;
4127
+ }
4128
+ async start() {
4129
+ await new Promise((resolve3) => {
4130
+ this.server = serve(
4131
+ {
4132
+ fetch: this.app.fetch,
4133
+ port: this.config.port
4134
+ },
4135
+ () => {
4136
+ this.logger.info(`HTTP server listening on port ${this.config.port}`);
4137
+ resolve3();
4138
+ }
4139
+ );
4140
+ });
4141
+ await this.autoInitializeSession();
4142
+ }
4143
+ async autoInitializeSession() {
4144
+ const { taskId, runId, mode, projectId } = this.config;
4145
+ this.logger.info("Auto-initializing session", { taskId, runId, mode });
4146
+ const payload = {
4147
+ task_id: taskId,
4148
+ run_id: runId,
4149
+ team_id: projectId,
4150
+ user_id: 0,
4151
+ // System-initiated
4152
+ distinct_id: "agent-server",
4153
+ mode
4154
+ };
4155
+ await this.initializeSession(payload, null);
4156
+ }
4157
+ async stop() {
4158
+ this.logger.info("Stopping agent server...");
4159
+ if (this.session) {
4160
+ await this.cleanupSession();
4161
+ }
4162
+ if (this.server) {
4163
+ this.server.close();
4164
+ this.server = null;
4165
+ }
4166
+ this.logger.info("Agent server stopped");
4167
+ }
4168
+ authenticateRequest(getHeader) {
4169
+ if (!this.config.jwtPublicKey) {
4170
+ throw new JwtValidationError(
4171
+ "Server not configured with JWT public key",
4172
+ "server_error"
4173
+ );
4174
+ }
4175
+ const authHeader = getHeader("authorization");
4176
+ if (!authHeader?.startsWith("Bearer ")) {
4177
+ throw new JwtValidationError(
4178
+ "Missing authorization header",
4179
+ "invalid_token"
4180
+ );
4181
+ }
4182
+ const token = authHeader.slice(7);
4183
+ return validateJwt(token, this.config.jwtPublicKey);
4184
+ }
4185
+ async executeCommand(method, params) {
4186
+ if (!this.session) {
4187
+ throw new Error("No active session");
4188
+ }
4189
+ switch (method) {
4190
+ case POSTHOG_NOTIFICATIONS.USER_MESSAGE:
4191
+ case "user_message": {
4192
+ const content = params.content;
4193
+ this.logger.info(
4194
+ `Processing user message: ${content.substring(0, 100)}...`
4195
+ );
4196
+ const result = await this.session.clientConnection.prompt({
4197
+ sessionId: this.session.payload.run_id,
4198
+ prompt: [{ type: "text", text: content }]
4199
+ });
4200
+ return { stopReason: result.stopReason };
4201
+ }
4202
+ case POSTHOG_NOTIFICATIONS.CANCEL:
4203
+ case "cancel": {
4204
+ this.logger.info("Cancel requested");
4205
+ await this.session.clientConnection.cancel({
4206
+ sessionId: this.session.payload.run_id
4207
+ });
4208
+ return { cancelled: true };
4209
+ }
4210
+ case POSTHOG_NOTIFICATIONS.CLOSE:
4211
+ case "close": {
4212
+ this.logger.info("Close requested");
4213
+ await this.cleanupSession();
4214
+ return { closed: true };
4215
+ }
4216
+ default:
4217
+ throw new Error(`Unknown method: ${method}`);
4218
+ }
4219
+ }
4220
+ async initializeSession(payload, sseController) {
4221
+ if (this.session) {
4222
+ await this.cleanupSession();
4223
+ }
4224
+ this.logger.info("Initializing session", {
4225
+ runId: payload.run_id,
4226
+ taskId: payload.task_id
4227
+ });
4228
+ const deviceInfo = {
4229
+ type: "cloud",
4230
+ name: process.env.HOSTNAME || "cloud-sandbox"
4231
+ };
4232
+ this.configureEnvironment();
4233
+ const treeTracker = new TreeTracker({
4234
+ repositoryPath: this.config.repositoryPath,
4235
+ taskId: payload.task_id,
4236
+ runId: payload.run_id,
4237
+ logger: new Logger({ debug: true, prefix: "[TreeTracker]" })
4238
+ });
4239
+ const _posthogAPI = new PostHogAPIClient({
4240
+ apiUrl: this.config.apiUrl,
4241
+ projectId: this.config.projectId,
4242
+ getApiKey: () => this.config.apiKey
4243
+ });
4244
+ const logWriter = new SessionLogWriter({
4245
+ otelConfig: {
4246
+ posthogHost: this.config.apiUrl,
4247
+ apiKey: this.config.apiKey,
4248
+ logsPath: "/i/v1/agent-logs"
4249
+ },
4250
+ logger: new Logger({ debug: true, prefix: "[SessionLogWriter]" })
4251
+ });
4252
+ const acpConnection = createAcpConnection({
4253
+ taskRunId: payload.run_id,
4254
+ taskId: payload.task_id,
4255
+ deviceType: deviceInfo.type,
4256
+ logWriter
4257
+ });
4258
+ const onAcpMessage = (message) => {
4259
+ this.broadcastEvent({
4260
+ type: "notification",
4261
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4262
+ notification: message
4263
+ });
4264
+ };
4265
+ const tappedReadable = createTappedReadableStream(
4266
+ acpConnection.clientStreams.readable,
4267
+ onAcpMessage,
4268
+ this.logger
4269
+ );
4270
+ const tappedWritable = createTappedWritableStream2(
4271
+ acpConnection.clientStreams.writable,
4272
+ onAcpMessage,
4273
+ this.logger
4274
+ );
4275
+ const clientStream = ndJsonStream2(tappedWritable, tappedReadable);
4276
+ const clientConnection = new ClientSideConnection(
4277
+ () => this.createCloudClient(payload),
4278
+ clientStream
4279
+ );
4280
+ await clientConnection.initialize({
4281
+ protocolVersion: PROTOCOL_VERSION,
4282
+ clientCapabilities: {}
4283
+ });
4284
+ await clientConnection.newSession({
4285
+ cwd: this.config.repositoryPath,
4286
+ mcpServers: [],
4287
+ _meta: { sessionId: payload.run_id }
4288
+ });
4289
+ this.session = {
4290
+ payload,
4291
+ acpConnection,
4292
+ clientConnection,
4293
+ treeTracker,
4294
+ sseController,
4295
+ deviceInfo,
4296
+ logWriter
4297
+ };
4298
+ this.logger.info("Session initialized successfully");
4299
+ await this.sendInitialTaskMessage(payload);
4300
+ }
4301
+ async sendInitialTaskMessage(payload) {
4302
+ if (!this.session) return;
4303
+ try {
4304
+ this.logger.info("Fetching task details", { taskId: payload.task_id });
4305
+ const task = await this.posthogAPI.getTask(payload.task_id);
4306
+ if (!task.description) {
4307
+ this.logger.warn("Task has no description, skipping initial message");
4308
+ return;
4309
+ }
4310
+ this.logger.info("Sending initial task message", {
4311
+ taskId: payload.task_id,
4312
+ descriptionLength: task.description.length
4313
+ });
4314
+ const result = await this.session.clientConnection.prompt({
4315
+ sessionId: payload.run_id,
4316
+ prompt: [{ type: "text", text: task.description }]
4317
+ });
4318
+ this.logger.info("Initial task message completed", {
4319
+ stopReason: result.stopReason
4320
+ });
4321
+ const mode = this.getEffectiveMode(payload);
4322
+ if (mode === "background") {
4323
+ await this.signalTaskComplete(payload, result.stopReason);
4324
+ } else {
4325
+ this.logger.info("Interactive mode - staying open for conversation");
4326
+ }
4327
+ } catch (error) {
4328
+ this.logger.error("Failed to send initial task message", error);
4329
+ const mode = this.getEffectiveMode(payload);
4330
+ if (mode === "background") {
4331
+ await this.signalTaskComplete(payload, "error");
4332
+ }
4333
+ }
4334
+ }
4335
+ async signalTaskComplete(payload, stopReason) {
4336
+ const status = stopReason === "cancelled" ? "cancelled" : stopReason === "error" ? "failed" : "completed";
4337
+ try {
4338
+ await this.posthogAPI.updateTaskRun(payload.task_id, payload.run_id, {
4339
+ status,
4340
+ error_message: stopReason === "error" ? "Agent error" : void 0
4341
+ });
4342
+ this.logger.info("Task completion signaled", { status, stopReason });
4343
+ } catch (error) {
4344
+ this.logger.error("Failed to signal task completion", error);
4345
+ }
4346
+ }
4347
+ configureEnvironment() {
4348
+ const { apiKey, apiUrl, projectId } = this.config;
4349
+ const gatewayUrl = process.env.LLM_GATEWAY_URL || getLlmGatewayUrl(apiUrl);
4350
+ const openaiBaseUrl = gatewayUrl.endsWith("/v1") ? gatewayUrl : `${gatewayUrl}/v1`;
4351
+ Object.assign(process.env, {
4352
+ // PostHog
4353
+ POSTHOG_API_KEY: apiKey,
4354
+ POSTHOG_API_URL: apiUrl,
4355
+ POSTHOG_API_HOST: apiUrl,
4356
+ POSTHOG_AUTH_HEADER: `Bearer ${apiKey}`,
4357
+ POSTHOG_PROJECT_ID: String(projectId),
4358
+ // Anthropic
4359
+ ANTHROPIC_API_KEY: apiKey,
4360
+ ANTHROPIC_AUTH_TOKEN: apiKey,
4361
+ ANTHROPIC_BASE_URL: gatewayUrl,
4362
+ // OpenAI (for models like GPT-4, o1, etc.)
4363
+ OPENAI_API_KEY: apiKey,
4364
+ OPENAI_BASE_URL: openaiBaseUrl,
4365
+ // Generic gateway
4366
+ LLM_GATEWAY_URL: gatewayUrl
4367
+ });
4368
+ }
4369
+ createCloudClient(payload) {
4370
+ const mode = this.getEffectiveMode(payload);
4371
+ return {
4372
+ requestPermission: async (params) => {
4373
+ this.logger.debug("Permission request", {
4374
+ mode,
4375
+ options: params.options
4376
+ });
4377
+ const allowOption = params.options.find(
4378
+ (o) => o.kind === "allow_once" || o.kind === "allow_always"
4379
+ );
4380
+ return {
4381
+ outcome: {
4382
+ outcome: "selected",
4383
+ optionId: allowOption?.optionId ?? params.options[0].optionId
4384
+ }
4385
+ };
4386
+ },
4387
+ sessionUpdate: async (params) => {
4388
+ if (params.update?.sessionUpdate === "tool_call_update") {
4389
+ const meta = params.update?._meta?.claudeCode;
4390
+ const toolName = meta?.toolName;
4391
+ const toolResponse = meta?.toolResponse;
4392
+ if ((toolName === "Write" || toolName === "Edit") && toolResponse?.filePath) {
4393
+ await this.captureTreeState();
4394
+ }
4395
+ }
4396
+ }
4397
+ };
4398
+ }
4399
+ async cleanupSession() {
4400
+ if (!this.session) return;
4401
+ this.logger.info("Cleaning up session");
4402
+ try {
4403
+ await this.captureTreeState();
4404
+ } catch (error) {
4405
+ this.logger.error("Failed to capture final tree state", error);
4406
+ }
4407
+ try {
4408
+ await this.session.logWriter.flush(this.session.payload.run_id);
4409
+ } catch (error) {
4410
+ this.logger.error("Failed to flush session logs", error);
4411
+ }
4412
+ try {
4413
+ await this.session.acpConnection.cleanup();
4414
+ } catch (error) {
4415
+ this.logger.error("Failed to cleanup ACP connection", error);
4416
+ }
4417
+ if (this.session.sseController) {
4418
+ this.session.sseController.close();
4419
+ }
4420
+ this.session = null;
4421
+ }
4422
+ async captureTreeState() {
4423
+ if (!this.session?.treeTracker) return;
4424
+ try {
4425
+ const snapshot = await this.session.treeTracker.captureTree({});
4426
+ if (snapshot) {
4427
+ const snapshotWithDevice = {
4428
+ ...snapshot,
4429
+ device: this.session.deviceInfo
4430
+ };
4431
+ this.broadcastEvent({
4432
+ type: "notification",
4433
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4434
+ notification: {
4435
+ jsonrpc: "2.0",
4436
+ method: POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT,
4437
+ params: snapshotWithDevice
4438
+ }
4439
+ });
4440
+ }
4441
+ } catch (error) {
4442
+ this.logger.error("Failed to capture tree state", error);
4443
+ }
4444
+ }
4445
+ broadcastEvent(event) {
4446
+ if (this.session?.sseController) {
4447
+ this.sendSseEvent(this.session.sseController, event);
4448
+ }
4449
+ }
4450
+ sendSseEvent(controller, data) {
4451
+ controller.send(data);
4452
+ }
4453
+ };
4454
+
4455
+ // src/server/bin.ts
4456
+ var envSchema = z4.object({
4457
+ JWT_PUBLIC_KEY: z4.string({
4458
+ required_error: "JWT_PUBLIC_KEY is required for authenticating client connections"
4459
+ }).min(1, "JWT_PUBLIC_KEY cannot be empty"),
4460
+ POSTHOG_API_URL: z4.string({
4461
+ required_error: "POSTHOG_API_URL is required for LLM gateway communication"
4462
+ }).url("POSTHOG_API_URL must be a valid URL"),
4463
+ POSTHOG_PERSONAL_API_KEY: z4.string({
4464
+ required_error: "POSTHOG_PERSONAL_API_KEY is required for authenticating with PostHog services"
4465
+ }).min(1, "POSTHOG_PERSONAL_API_KEY cannot be empty"),
4466
+ POSTHOG_PROJECT_ID: z4.string({
4467
+ required_error: "POSTHOG_PROJECT_ID is required for routing requests to the correct project"
4468
+ }).regex(/^\d+$/, "POSTHOG_PROJECT_ID must be a numeric string").transform((val) => parseInt(val, 10))
4469
+ });
4470
+ var program = new Command();
4471
+ program.name("agent-server").description("PostHog cloud agent server - runs in sandbox environments").option("--port <port>", "HTTP server port", "3001").option(
4472
+ "--mode <mode>",
4473
+ "Execution mode: interactive or background",
4474
+ "interactive"
4475
+ ).requiredOption("--repositoryPath <path>", "Path to the repository").requiredOption("--taskId <id>", "Task ID").requiredOption("--runId <id>", "Task run ID").action(async (options) => {
4476
+ const envResult = envSchema.safeParse(process.env);
4477
+ if (!envResult.success) {
4478
+ const errors = envResult.error.issues.map((issue) => ` - ${issue.message}`).join("\n");
4479
+ program.error(`Environment validation failed:
4480
+ ${errors}`);
4481
+ return;
4482
+ }
4483
+ const env = envResult.data;
4484
+ const mode = options.mode === "background" ? "background" : "interactive";
4485
+ const server = new AgentServer({
4486
+ port: parseInt(options.port, 10),
4487
+ jwtPublicKey: env.JWT_PUBLIC_KEY,
4488
+ repositoryPath: options.repositoryPath,
4489
+ apiUrl: env.POSTHOG_API_URL,
4490
+ apiKey: env.POSTHOG_PERSONAL_API_KEY,
4491
+ projectId: env.POSTHOG_PROJECT_ID,
4492
+ mode,
4493
+ taskId: options.taskId,
4494
+ runId: options.runId
4495
+ });
4496
+ process.on("SIGINT", async () => {
4497
+ await server.stop();
4498
+ process.exit(0);
4499
+ });
4500
+ process.on("SIGTERM", async () => {
4501
+ await server.stop();
4502
+ process.exit(0);
4503
+ });
4504
+ await server.start();
4505
+ });
4506
+ program.parse();
4507
+ //# sourceMappingURL=bin.js.map