@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
@@ -1,693 +1,58 @@
1
- import type {
2
- PlanEntry,
3
- ToolCallContent,
4
- ToolCallLocation,
5
- ToolKind,
6
- } from "@agentclientprotocol/sdk";
7
- import type { HookCallback, HookInput } from "@anthropic-ai/claude-agent-sdk";
8
- import type {
9
- ToolResultBlockParam,
10
- WebSearchToolResultBlockParam,
11
- } from "@anthropic-ai/sdk/resources";
12
- import type {
13
- BetaBashCodeExecutionToolResultBlockParam,
14
- BetaCodeExecutionToolResultBlockParam,
15
- BetaRequestMCPToolResultBlockParam,
16
- BetaTextEditorCodeExecutionToolResultBlockParam,
17
- BetaToolSearchToolResultBlockParam,
18
- BetaWebFetchToolResultBlockParam,
19
- BetaWebSearchToolResultBlockParam,
20
- } from "@anthropic-ai/sdk/resources/beta.mjs";
21
- import { Logger } from "@/utils/logger.js";
22
- import {
23
- replaceAndCalculateLocation,
24
- SYSTEM_REMINDER,
25
- toolNames,
26
- } from "./mcp-server.js";
27
-
28
- interface ToolInfo {
29
- title: string;
30
- kind: ToolKind;
31
- content: ToolCallContent[];
32
- locations?: ToolCallLocation[];
33
- }
34
-
35
- interface ToolUpdate {
36
- title?: string;
37
- content?: ToolCallContent[];
38
- locations?: ToolCallLocation[];
39
- }
40
-
41
- interface ToolUse {
42
- name: string;
43
- input?: unknown;
44
- }
45
-
46
- export function toolInfoFromToolUse(
47
- toolUse: ToolUse,
48
- cachedFileContent: { [key: string]: string },
49
- logger: Logger = new Logger({ debug: false, prefix: "[ClaudeTools]" }),
50
- ): ToolInfo {
51
- const name = toolUse.name;
52
- // Cast input to allow property access - each case handles its expected properties
53
- const input = toolUse.input as Record<string, unknown> | undefined;
54
-
55
- switch (name) {
56
- case "Task":
57
- return {
58
- title: input?.description ? String(input.description) : "Task",
59
- kind: "think",
60
- content: input?.prompt
61
- ? [
62
- {
63
- type: "content",
64
- content: { type: "text", text: String(input.prompt) },
65
- },
66
- ]
67
- : [],
68
- };
69
-
70
- case "NotebookRead":
71
- return {
72
- title: input?.notebook_path
73
- ? `Read Notebook ${String(input.notebook_path)}`
74
- : "Read Notebook",
75
- kind: "read",
76
- content: [],
77
- locations: input?.notebook_path
78
- ? [{ path: String(input.notebook_path) }]
79
- : [],
80
- };
81
-
82
- case "NotebookEdit":
83
- return {
84
- title: input?.notebook_path
85
- ? `Edit Notebook ${String(input.notebook_path)}`
86
- : "Edit Notebook",
87
- kind: "edit",
88
- content: input?.new_source
89
- ? [
90
- {
91
- type: "content",
92
- content: { type: "text", text: String(input.new_source) },
93
- },
94
- ]
95
- : [],
96
- locations: input?.notebook_path
97
- ? [{ path: String(input.notebook_path) }]
98
- : [],
99
- };
100
-
101
- case "Bash":
102
- case toolNames.bash:
103
- return {
104
- title: input?.command
105
- ? `\`${String(input.command).replaceAll("`", "\\`")}\``
106
- : "Terminal",
107
- kind: "execute",
108
- content: input?.description
109
- ? [
110
- {
111
- type: "content",
112
- content: { type: "text", text: String(input.description) },
113
- },
114
- ]
115
- : [],
116
- };
117
-
118
- case "BashOutput":
119
- case toolNames.bashOutput:
120
- return {
121
- title: "Tail Logs",
122
- kind: "execute",
123
- content: [],
124
- };
125
-
126
- case "KillShell":
127
- case toolNames.killShell:
128
- return {
129
- title: "Kill Process",
130
- kind: "execute",
131
- content: [],
132
- };
133
-
134
- case toolNames.read: {
135
- let limit = "";
136
- const inputLimit = input?.limit as number | undefined;
137
- const inputOffset = (input?.offset as number | undefined) ?? 0;
138
- if (inputLimit) {
139
- limit = ` (${inputOffset + 1} - ${inputOffset + inputLimit})`;
140
- } else if (inputOffset) {
141
- limit = ` (from line ${inputOffset + 1})`;
142
- }
143
- return {
144
- title: `Read ${input?.file_path ? String(input.file_path) : "File"}${limit}`,
145
- kind: "read",
146
- locations: input?.file_path
147
- ? [
148
- {
149
- path: String(input.file_path),
150
- line: inputOffset,
151
- },
152
- ]
153
- : [],
154
- content: [],
155
- };
156
- }
157
-
158
- case "Read":
159
- return {
160
- title: "Read File",
161
- kind: "read",
162
- content: [],
163
- locations: input?.file_path
164
- ? [
165
- {
166
- path: String(input.file_path),
167
- line: (input?.offset as number | undefined) ?? 0,
168
- },
169
- ]
170
- : [],
171
- };
172
-
173
- case "LS":
174
- return {
175
- title: `List the ${input?.path ? `\`${String(input.path)}\`` : "current"} directory's contents`,
176
- kind: "search",
177
- content: [],
178
- locations: [],
179
- };
180
-
181
- case toolNames.edit:
182
- case "Edit": {
183
- const path = input?.file_path ? String(input.file_path) : undefined;
184
- let oldText = input?.old_string ? String(input.old_string) : null;
185
- let newText = input?.new_string ? String(input.new_string) : "";
186
- let affectedLines: number[] = [];
187
-
188
- if (path && oldText) {
189
- try {
190
- const oldContent = cachedFileContent[path] || "";
191
- const newContent = replaceAndCalculateLocation(oldContent, [
192
- {
193
- oldText,
194
- newText,
195
- replaceAll: false,
196
- },
197
- ]);
198
- oldText = oldContent;
199
- newText = newContent.newContent;
200
- affectedLines = newContent.lineNumbers;
201
- } catch (e) {
202
- logger.error("Failed to edit file", e);
203
- }
204
- }
205
- return {
206
- title: path ? `Edit \`${path}\`` : "Edit",
207
- kind: "edit",
208
- content:
209
- input && path
210
- ? [
211
- {
212
- type: "diff",
213
- path,
214
- oldText,
215
- newText,
216
- },
217
- ]
218
- : [],
219
- locations: path
220
- ? affectedLines.length > 0
221
- ? affectedLines.map((line) => ({ line, path }))
222
- : [{ path }]
223
- : [],
224
- };
225
- }
226
-
227
- case toolNames.write: {
228
- let contentResult: ToolCallContent[] = [];
229
- const filePath = input?.file_path ? String(input.file_path) : undefined;
230
- const contentStr = input?.content ? String(input.content) : undefined;
231
- if (filePath) {
232
- contentResult = [
233
- {
234
- type: "diff",
235
- path: filePath,
236
- oldText: null,
237
- newText: contentStr ?? "",
238
- },
239
- ];
240
- } else if (contentStr) {
241
- contentResult = [
242
- {
243
- type: "content",
244
- content: { type: "text", text: contentStr },
245
- },
246
- ];
247
- }
248
- return {
249
- title: filePath ? `Write ${filePath}` : "Write",
250
- kind: "edit",
251
- content: contentResult,
252
- locations: filePath ? [{ path: filePath }] : [],
253
- };
254
- }
255
-
256
- case "Write": {
257
- const filePath = input?.file_path ? String(input.file_path) : undefined;
258
- const contentStr = input?.content ? String(input.content) : "";
259
- return {
260
- title: filePath ? `Write ${filePath}` : "Write",
261
- kind: "edit",
262
- content: filePath
263
- ? [
264
- {
265
- type: "diff",
266
- path: filePath,
267
- oldText: null,
268
- newText: contentStr,
269
- },
270
- ]
271
- : [],
272
- locations: filePath ? [{ path: filePath }] : [],
273
- };
274
- }
275
-
276
- case "Glob": {
277
- let label = "Find";
278
- const pathStr = input?.path ? String(input.path) : undefined;
279
- if (pathStr) {
280
- label += ` \`${pathStr}\``;
281
- }
282
- if (input?.pattern) {
283
- label += ` \`${String(input.pattern)}\``;
284
- }
285
- return {
286
- title: label,
287
- kind: "search",
288
- content: [],
289
- locations: pathStr ? [{ path: pathStr }] : [],
290
- };
291
- }
292
-
293
- case "Grep": {
294
- let label = "grep";
295
-
296
- if (input?.["-i"]) {
297
- label += " -i";
298
- }
299
- if (input?.["-n"]) {
300
- label += " -n";
301
- }
302
-
303
- if (input?.["-A"] !== undefined) {
304
- label += ` -A ${input["-A"]}`;
305
- }
306
- if (input?.["-B"] !== undefined) {
307
- label += ` -B ${input["-B"]}`;
308
- }
309
- if (input?.["-C"] !== undefined) {
310
- label += ` -C ${input["-C"]}`;
311
- }
312
-
313
- if (input?.output_mode) {
314
- switch (input.output_mode) {
315
- case "FilesWithMatches":
316
- label += " -l";
317
- break;
318
- case "Count":
319
- label += " -c";
320
- break;
321
- default:
322
- break;
323
- }
324
- }
325
-
326
- if (input?.head_limit !== undefined) {
327
- label += ` | head -${input.head_limit}`;
328
- }
329
-
330
- if (input?.glob) {
331
- label += ` --include="${String(input.glob)}"`;
332
- }
333
-
334
- if (input?.type) {
335
- label += ` --type=${String(input.type)}`;
336
- }
337
-
338
- if (input?.multiline) {
339
- label += " -P";
340
- }
341
-
342
- label += ` "${input?.pattern ? String(input.pattern) : ""}"`;
343
-
344
- if (input?.path) {
345
- label += ` ${String(input.path)}`;
346
- }
347
-
348
- return {
349
- title: label,
350
- kind: "search",
351
- content: [],
352
- };
353
- }
354
-
355
- case "WebFetch":
356
- return {
357
- title: input?.url ? `Fetch ${String(input.url)}` : "Fetch",
358
- kind: "fetch",
359
- content: input?.prompt
360
- ? [
361
- {
362
- type: "content",
363
- content: { type: "text", text: String(input.prompt) },
364
- },
365
- ]
366
- : [],
367
- };
368
-
369
- case "WebSearch": {
370
- let label = `"${input?.query ? String(input.query) : ""}"`;
371
- const allowedDomains = input?.allowed_domains as string[] | undefined;
372
- const blockedDomains = input?.blocked_domains as string[] | undefined;
373
-
374
- if (allowedDomains && allowedDomains.length > 0) {
375
- label += ` (allowed: ${allowedDomains.join(", ")})`;
376
- }
377
-
378
- if (blockedDomains && blockedDomains.length > 0) {
379
- label += ` (blocked: ${blockedDomains.join(", ")})`;
380
- }
381
-
382
- return {
383
- title: label,
384
- kind: "fetch",
385
- content: [],
386
- };
387
- }
388
-
389
- case "TodoWrite":
390
- return {
391
- title: Array.isArray(input?.todos)
392
- ? `Update TODOs: ${input.todos.map((todo: { content?: string }) => todo.content).join(", ")}`
393
- : "Update TODOs",
394
- kind: "think",
395
- content: [],
396
- };
397
-
398
- case "ExitPlanMode":
399
- return {
400
- title: "Ready to code?",
401
- kind: "switch_mode",
402
- content: input?.plan
403
- ? [
404
- {
405
- type: "content",
406
- content: { type: "text", text: String(input.plan) },
407
- },
408
- ]
409
- : [],
410
- };
411
-
412
- case "AskUserQuestion": {
413
- const questions = input?.questions as
414
- | Array<{ question?: string }>
415
- | undefined;
416
- return {
417
- title: questions?.[0]?.question || "Question",
418
- kind: "ask" as ToolKind,
419
- content: questions
420
- ? [
421
- {
422
- type: "content",
423
- content: {
424
- type: "text",
425
- text: JSON.stringify(questions, null, 2),
426
- },
427
- },
428
- ]
429
- : [],
430
- };
431
- }
432
-
433
- case "Other": {
434
- let output: string;
435
- try {
436
- output = JSON.stringify(input, null, 2);
437
- } catch {
438
- output = typeof input === "string" ? input : "{}";
439
- }
440
- return {
441
- title: name || "Unknown Tool",
442
- kind: "other",
443
- content: [
444
- {
445
- type: "content",
446
- content: {
447
- type: "text",
448
- text: `\`\`\`json\n${output}\`\`\``,
449
- },
450
- },
451
- ],
452
- };
453
- }
454
-
455
- default:
456
- return {
457
- title: name || "Unknown Tool",
458
- kind: "other",
459
- content: [],
460
- };
461
- }
462
- }
463
-
464
- export function toolUpdateFromToolResult(
465
- toolResult:
466
- | ToolResultBlockParam
467
- | BetaWebSearchToolResultBlockParam
468
- | BetaWebFetchToolResultBlockParam
469
- | WebSearchToolResultBlockParam
470
- | BetaCodeExecutionToolResultBlockParam
471
- | BetaBashCodeExecutionToolResultBlockParam
472
- | BetaTextEditorCodeExecutionToolResultBlockParam
473
- | BetaRequestMCPToolResultBlockParam
474
- | BetaToolSearchToolResultBlockParam,
475
- toolUse: ToolUse | undefined,
476
- ): ToolUpdate {
477
- switch (toolUse?.name) {
478
- case "Read":
479
- case toolNames.read:
480
- if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
481
- return {
482
- content: toolResult.content.map((item) => {
483
- const itemObj = item as { type?: string; text?: string };
484
- if (itemObj.type === "text") {
485
- return {
486
- type: "content" as const,
487
- content: {
488
- type: "text" as const,
489
- text: markdownEscape(
490
- (itemObj.text ?? "").replace(SYSTEM_REMINDER, ""),
491
- ),
492
- },
493
- };
494
- }
495
- // For non-text content, return as-is with proper typing
496
- return {
497
- type: "content" as const,
498
- content: item as { type: "text"; text: string },
499
- };
500
- }),
501
- };
502
- } else if (
503
- typeof toolResult.content === "string" &&
504
- toolResult.content.length > 0
505
- ) {
506
- return {
507
- content: [
508
- {
509
- type: "content",
510
- content: {
511
- type: "text",
512
- text: markdownEscape(
513
- toolResult.content.replace(SYSTEM_REMINDER, ""),
514
- ),
515
- },
516
- },
517
- ],
518
- };
519
- }
520
- return {};
521
-
522
- case toolNames.bash:
523
- case "edit":
524
- case "Edit":
525
- case toolNames.edit:
526
- case toolNames.write:
527
- case "Write": {
528
- if (
529
- "is_error" in toolResult &&
530
- toolResult.is_error &&
531
- toolResult.content &&
532
- toolResult.content.length > 0
533
- ) {
534
- // Only return errors
535
- return toAcpContentUpdate(toolResult.content, true);
536
- }
537
- return {};
538
- }
1
+ export {
2
+ getAvailableModes,
3
+ type ModeInfo,
4
+ TWIG_EXECUTION_MODES,
5
+ type TwigExecutionMode,
6
+ } from "../../execution-mode.js";
7
+
8
+ import type { TwigExecutionMode } from "../../execution-mode.js";
9
+ import { isMcpToolReadOnly } from "./mcp/tool-metadata.js";
10
+
11
+ export const READ_TOOLS: Set<string> = new Set(["Read", "NotebookRead"]);
12
+
13
+ export const WRITE_TOOLS: Set<string> = new Set([
14
+ "Edit",
15
+ "Write",
16
+ "NotebookEdit",
17
+ ]);
18
+
19
+ export const BASH_TOOLS: Set<string> = new Set([
20
+ "Bash",
21
+ "BashOutput",
22
+ "KillShell",
23
+ ]);
24
+
25
+ export const SEARCH_TOOLS: Set<string> = new Set(["Glob", "Grep", "LS"]);
26
+
27
+ export const WEB_TOOLS: Set<string> = new Set(["WebSearch", "WebFetch"]);
28
+
29
+ export const AGENT_TOOLS: Set<string> = new Set(["Task", "TodoWrite"]);
30
+
31
+ const BASE_ALLOWED_TOOLS = [
32
+ ...READ_TOOLS,
33
+ ...SEARCH_TOOLS,
34
+ ...WEB_TOOLS,
35
+ ...AGENT_TOOLS,
36
+ ];
37
+
38
+ const AUTO_ALLOWED_TOOLS: Record<string, Set<string>> = {
39
+ default: new Set(BASE_ALLOWED_TOOLS),
40
+ acceptEdits: new Set([...BASE_ALLOWED_TOOLS, ...WRITE_TOOLS]),
41
+ plan: new Set(BASE_ALLOWED_TOOLS),
42
+ };
539
43
 
540
- case "ExitPlanMode": {
541
- return { title: "Exited Plan Mode" };
542
- }
543
- case "AskUserQuestion": {
544
- // The answer is returned in the tool result
545
- const content = toolResult.content;
546
- if (Array.isArray(content) && content.length > 0) {
547
- const firstItem = content[0];
548
- if (
549
- typeof firstItem === "object" &&
550
- firstItem !== null &&
551
- "text" in firstItem
552
- ) {
553
- return {
554
- title: "Answer received",
555
- content: [
556
- {
557
- type: "content",
558
- content: { type: "text", text: String(firstItem.text) },
559
- },
560
- ],
561
- };
562
- }
563
- }
564
- return { title: "Question answered" };
565
- }
566
- default: {
567
- return toAcpContentUpdate(
568
- toolResult.content,
569
- "is_error" in toolResult ? toolResult.is_error : false,
570
- );
571
- }
44
+ export function isToolAllowedForMode(
45
+ toolName: string,
46
+ mode: TwigExecutionMode,
47
+ ): boolean {
48
+ if (mode === "bypassPermissions") {
49
+ return true;
572
50
  }
573
- }
574
-
575
- function toAcpContentUpdate(
576
- content: unknown,
577
- isError: boolean = false,
578
- ): { content?: ToolCallContent[] } {
579
- if (Array.isArray(content) && content.length > 0) {
580
- return {
581
- content: content.map((item) => {
582
- const itemObj = item as { type?: string; text?: string };
583
- if (isError && itemObj.type === "text") {
584
- return {
585
- type: "content" as const,
586
- content: {
587
- type: "text" as const,
588
- text: `\`\`\`\n${itemObj.text ?? ""}\n\`\`\``,
589
- },
590
- };
591
- }
592
- return {
593
- type: "content" as const,
594
- content: item as { type: "text"; text: string },
595
- };
596
- }),
597
- };
598
- } else if (typeof content === "string" && content.length > 0) {
599
- return {
600
- content: [
601
- {
602
- type: "content",
603
- content: {
604
- type: "text",
605
- text: isError ? `\`\`\`\n${content}\n\`\`\`` : content,
606
- },
607
- },
608
- ],
609
- };
51
+ if (AUTO_ALLOWED_TOOLS[mode]?.has(toolName) === true) {
52
+ return true;
610
53
  }
611
- return {};
612
- }
613
-
614
- export type ClaudePlanEntry = {
615
- content: string;
616
- status: "pending" | "in_progress" | "completed";
617
- activeForm: string;
618
- };
619
-
620
- export function planEntries(input: { todos: ClaudePlanEntry[] }): PlanEntry[] {
621
- return input.todos.map((input) => ({
622
- content: input.content,
623
- status: input.status,
624
- priority: "medium",
625
- }));
626
- }
627
-
628
- export function markdownEscape(text: string): string {
629
- let escapedText = "```";
630
- for (const [m] of text.matchAll(/^```+/gm)) {
631
- while (m.length >= escapedText.length) {
632
- escapedText += "`";
633
- }
54
+ if (isMcpToolReadOnly(toolName)) {
55
+ return true;
634
56
  }
635
- return `${escapedText}\n${text}${text.endsWith("\n") ? "" : "\n"}${escapedText}`;
57
+ return false;
636
58
  }
637
-
638
- /* A global variable to store callbacks that should be executed when receiving hooks from Claude Code */
639
- const toolUseCallbacks: {
640
- [toolUseId: string]: {
641
- onPostToolUseHook?: (
642
- toolUseID: string,
643
- toolInput: unknown,
644
- toolResponse: unknown,
645
- ) => Promise<void>;
646
- };
647
- } = {};
648
-
649
- /* Setup callbacks that will be called when receiving hooks from Claude Code */
650
- export const registerHookCallback = (
651
- toolUseID: string,
652
- {
653
- onPostToolUseHook,
654
- }: {
655
- onPostToolUseHook?: (
656
- toolUseID: string,
657
- toolInput: unknown,
658
- toolResponse: unknown,
659
- ) => Promise<void>;
660
- },
661
- ) => {
662
- toolUseCallbacks[toolUseID] = {
663
- onPostToolUseHook,
664
- };
665
- };
666
-
667
- /* A callback for Claude Code that is called when receiving a PostToolUse hook */
668
- export const createPostToolUseHook =
669
- (
670
- logger: Logger = new Logger({ prefix: "[createPostToolUseHook]" }),
671
- ): HookCallback =>
672
- async (
673
- input: HookInput,
674
- toolUseID: string | undefined,
675
- ): Promise<{ continue: boolean }> => {
676
- if (input.hook_event_name === "PostToolUse" && toolUseID) {
677
- const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook;
678
- if (onPostToolUseHook) {
679
- await onPostToolUseHook(
680
- toolUseID,
681
- input.tool_input,
682
- input.tool_response,
683
- );
684
- delete toolUseCallbacks[toolUseID]; // Cleanup after execution
685
- } else {
686
- logger.error(
687
- `No onPostToolUseHook found for tool use ID: ${toolUseID}`,
688
- );
689
- delete toolUseCallbacks[toolUseID];
690
- }
691
- }
692
- return { continue: true };
693
- };