@posthog/agent 2.0.0 → 2.0.1

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