@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,92 @@
1
+ import type { ToolCallContent, ToolKind } from "@agentclientprotocol/sdk";
2
+ import { z } from "zod";
3
+ import type { PermissionOption } from "../permissions/permission-options.js";
4
+
5
+ export const OPTION_PREFIX = "option_";
6
+
7
+ export const QuestionOptionSchema = z.object({
8
+ label: z.string(),
9
+ description: z.string().optional(),
10
+ });
11
+
12
+ export const QuestionItemSchema = z.object({
13
+ question: z.string(),
14
+ header: z.string().optional(),
15
+ options: z.array(QuestionOptionSchema),
16
+ multiSelect: z.boolean().optional(),
17
+ completed: z.boolean().optional(),
18
+ });
19
+
20
+ export const QuestionMetaSchema = z.object({
21
+ questions: z.array(QuestionItemSchema),
22
+ });
23
+
24
+ export type QuestionOption = z.infer<typeof QuestionOptionSchema>;
25
+ export type QuestionItem = z.infer<typeof QuestionItemSchema>;
26
+ export type QuestionMeta = z.infer<typeof QuestionMetaSchema>;
27
+
28
+ export interface AskUserQuestionInput {
29
+ questions?: QuestionItem[];
30
+ question?: string;
31
+ header?: string;
32
+ options?: QuestionOption[];
33
+ multiSelect?: boolean;
34
+ }
35
+
36
+ export function normalizeAskUserQuestionInput(
37
+ input: AskUserQuestionInput,
38
+ ): QuestionItem[] | null {
39
+ if (input.questions && input.questions.length > 0) {
40
+ return input.questions;
41
+ }
42
+
43
+ if (input.question) {
44
+ return [
45
+ {
46
+ question: input.question,
47
+ header: input.header,
48
+ options: input.options || [],
49
+ multiSelect: input.multiSelect,
50
+ },
51
+ ];
52
+ }
53
+
54
+ return null;
55
+ }
56
+
57
+ interface QuestionToolCallData {
58
+ toolCallId: string;
59
+ title: string;
60
+ kind: ToolKind;
61
+ content: ToolCallContent[];
62
+ _meta: {
63
+ twigToolKind: "question";
64
+ questions: QuestionItem[];
65
+ };
66
+ }
67
+
68
+ export function buildQuestionToolCallData(
69
+ questions: QuestionItem[],
70
+ ): QuestionToolCallData {
71
+ return {
72
+ toolCallId: `question-${Date.now()}`,
73
+ title: questions[0]?.question ?? "Question",
74
+ kind: "other",
75
+ content: [],
76
+ _meta: {
77
+ twigToolKind: "question",
78
+ questions,
79
+ },
80
+ };
81
+ }
82
+
83
+ export function buildQuestionOptions(
84
+ question: QuestionItem,
85
+ ): PermissionOption[] {
86
+ return question.options.map((opt, idx) => ({
87
+ kind: "allow_once" as const,
88
+ name: opt.label,
89
+ optionId: `${OPTION_PREFIX}${idx}`,
90
+ _meta: opt.description ? { description: opt.description } : undefined,
91
+ }));
92
+ }
@@ -0,0 +1,38 @@
1
+ import type { AvailableCommand } from "@agentclientprotocol/sdk";
2
+ import type { Query } from "@anthropic-ai/claude-agent-sdk";
3
+
4
+ const UNSUPPORTED_COMMANDS = [
5
+ "context",
6
+ "cost",
7
+ "login",
8
+ "logout",
9
+ "output-style:new",
10
+ "release-notes",
11
+ "todos",
12
+ ];
13
+
14
+ export async function getAvailableSlashCommands(
15
+ q: Query,
16
+ ): Promise<AvailableCommand[]> {
17
+ const commands = await q.supportedCommands();
18
+
19
+ return commands
20
+ .map((command) => {
21
+ const input = command.argumentHint
22
+ ? { hint: command.argumentHint }
23
+ : null;
24
+ let name = command.name;
25
+ if (command.name.endsWith(" (MCP)")) {
26
+ name = `mcp:${name.replace(" (MCP)", "")}`;
27
+ }
28
+ return {
29
+ name,
30
+ description: command.description || "",
31
+ input,
32
+ };
33
+ })
34
+ .filter(
35
+ (command: AvailableCommand) =>
36
+ !UNSUPPORTED_COMMANDS.includes(command.name),
37
+ );
38
+ }
@@ -0,0 +1,37 @@
1
+ import type {
2
+ LoadSessionRequest,
3
+ NewSessionRequest,
4
+ } from "@agentclientprotocol/sdk";
5
+ import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
6
+
7
+ export function parseMcpServers(
8
+ params: NewSessionRequest | LoadSessionRequest,
9
+ ): Record<string, McpServerConfig> {
10
+ const mcpServers: Record<string, McpServerConfig> = {};
11
+ if (!Array.isArray(params.mcpServers)) {
12
+ return mcpServers;
13
+ }
14
+
15
+ for (const server of params.mcpServers) {
16
+ if ("type" in server) {
17
+ mcpServers[server.name] = {
18
+ type: server.type,
19
+ url: server.url,
20
+ headers: server.headers
21
+ ? Object.fromEntries(server.headers.map((e) => [e.name, e.value]))
22
+ : undefined,
23
+ };
24
+ } else {
25
+ mcpServers[server.name] = {
26
+ type: "stdio",
27
+ command: server.command,
28
+ args: server.args,
29
+ env: server.env
30
+ ? Object.fromEntries(server.env.map((e) => [e.name, e.value]))
31
+ : undefined,
32
+ };
33
+ }
34
+ }
35
+
36
+ return mcpServers;
37
+ }
@@ -0,0 +1,12 @@
1
+ export const DEFAULT_MODEL = "opus";
2
+
3
+ const GATEWAY_TO_SDK_MODEL: Record<string, string> = {
4
+ "claude-opus-4-5": "opus",
5
+ "claude-opus-4-6": "opus",
6
+ "claude-sonnet-4-5": "sonnet",
7
+ "claude-haiku-4-5": "haiku",
8
+ };
9
+
10
+ export function toSdkModelId(modelId: string): string {
11
+ return GATEWAY_TO_SDK_MODEL[modelId] ?? modelId;
12
+ }
@@ -0,0 +1,236 @@
1
+ import { spawn } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import type {
6
+ McpServerConfig,
7
+ Options,
8
+ SpawnedProcess,
9
+ SpawnOptions,
10
+ } from "@anthropic-ai/claude-agent-sdk";
11
+ import { IS_ROOT } from "../../../utils/common.js";
12
+ import type { Logger } from "../../../utils/logger.js";
13
+ import { createPostToolUseHook, type OnModeChange } from "../hooks.js";
14
+ import type { TwigExecutionMode } from "../tools.js";
15
+
16
+ export interface ProcessSpawnedInfo {
17
+ pid: number;
18
+ command: string;
19
+ sessionId: string;
20
+ }
21
+
22
+ export interface BuildOptionsParams {
23
+ cwd: string;
24
+ mcpServers: Record<string, McpServerConfig>;
25
+ permissionMode: TwigExecutionMode;
26
+ canUseTool: Options["canUseTool"];
27
+ logger: Logger;
28
+ systemPrompt?: Options["systemPrompt"];
29
+ userProvidedOptions?: Options;
30
+ sessionId?: string;
31
+ additionalDirectories?: string[];
32
+ onModeChange?: OnModeChange;
33
+ onProcessSpawned?: (info: ProcessSpawnedInfo) => void;
34
+ onProcessExited?: (pid: number) => void;
35
+ }
36
+
37
+ const BRANCH_NAMING_INSTRUCTIONS = `
38
+ # Branch Naming
39
+
40
+ Before pushing a "workspace-*" branch to origin, rename it to something descriptive based on the work done. Do this automatically without asking the user.
41
+ `;
42
+
43
+ export function buildSystemPrompt(
44
+ customPrompt?: unknown,
45
+ ): Options["systemPrompt"] {
46
+ const defaultPrompt: Options["systemPrompt"] = {
47
+ type: "preset",
48
+ preset: "claude_code",
49
+ append: BRANCH_NAMING_INSTRUCTIONS,
50
+ };
51
+
52
+ if (!customPrompt) {
53
+ return defaultPrompt;
54
+ }
55
+
56
+ if (typeof customPrompt === "string") {
57
+ return customPrompt + BRANCH_NAMING_INSTRUCTIONS;
58
+ }
59
+
60
+ if (
61
+ typeof customPrompt === "object" &&
62
+ customPrompt !== null &&
63
+ "append" in customPrompt &&
64
+ typeof customPrompt.append === "string"
65
+ ) {
66
+ return {
67
+ ...defaultPrompt,
68
+ append: customPrompt.append + BRANCH_NAMING_INSTRUCTIONS,
69
+ };
70
+ }
71
+
72
+ return defaultPrompt;
73
+ }
74
+
75
+ function buildMcpServers(
76
+ userServers: Record<string, McpServerConfig> | undefined,
77
+ acpServers: Record<string, McpServerConfig>,
78
+ ): Record<string, McpServerConfig> {
79
+ return {
80
+ ...(userServers || {}),
81
+ ...acpServers,
82
+ };
83
+ }
84
+
85
+ function buildEnvironment(): Record<string, string> {
86
+ return {
87
+ ...process.env,
88
+ ELECTRON_RUN_AS_NODE: "1",
89
+ CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL: "true",
90
+ };
91
+ }
92
+
93
+ function buildHooks(
94
+ userHooks: Options["hooks"],
95
+ onModeChange?: OnModeChange,
96
+ ): Options["hooks"] {
97
+ return {
98
+ ...userHooks,
99
+ PostToolUse: [
100
+ ...(userHooks?.PostToolUse || []),
101
+ {
102
+ hooks: [createPostToolUseHook({ onModeChange })],
103
+ },
104
+ ],
105
+ };
106
+ }
107
+
108
+ function getAbortController(
109
+ userProvidedController: AbortController | undefined,
110
+ ): AbortController {
111
+ const controller = userProvidedController ?? new AbortController();
112
+ if (controller.signal.aborted) {
113
+ throw new Error("Cancelled");
114
+ }
115
+ return controller;
116
+ }
117
+
118
+ function buildSpawnWrapper(
119
+ sessionId: string,
120
+ onProcessSpawned: (info: ProcessSpawnedInfo) => void,
121
+ onProcessExited?: (pid: number) => void,
122
+ ): (options: SpawnOptions) => SpawnedProcess {
123
+ return (spawnOpts: SpawnOptions): SpawnedProcess => {
124
+ const child = spawn(spawnOpts.command, spawnOpts.args, {
125
+ cwd: spawnOpts.cwd,
126
+ env: spawnOpts.env as NodeJS.ProcessEnv,
127
+ stdio: ["pipe", "pipe", "pipe"],
128
+ });
129
+
130
+ if (child.pid) {
131
+ onProcessSpawned({
132
+ pid: child.pid,
133
+ command: `${spawnOpts.command} ${spawnOpts.args.join(" ")}`,
134
+ sessionId,
135
+ });
136
+ }
137
+
138
+ if (onProcessExited) {
139
+ child.on("exit", () => {
140
+ if (child.pid) {
141
+ onProcessExited(child.pid);
142
+ }
143
+ });
144
+ }
145
+
146
+ // Listen for abort signal
147
+ if (spawnOpts.signal) {
148
+ spawnOpts.signal.addEventListener("abort", () => {
149
+ child.kill("SIGTERM");
150
+ });
151
+ }
152
+
153
+ return {
154
+ stdin: child.stdin!,
155
+ stdout: child.stdout!,
156
+ get killed() {
157
+ return child.killed;
158
+ },
159
+ get exitCode() {
160
+ return child.exitCode;
161
+ },
162
+ kill(signal: NodeJS.Signals) {
163
+ return child.kill(signal);
164
+ },
165
+ on(event: "exit" | "error", listener: (...args: any[]) => void) {
166
+ child.on(event, listener);
167
+ },
168
+ once(event: "exit" | "error", listener: (...args: any[]) => void) {
169
+ child.once(event, listener);
170
+ },
171
+ off(event: "exit" | "error", listener: (...args: any[]) => void) {
172
+ child.off(event, listener);
173
+ },
174
+ };
175
+ };
176
+ }
177
+
178
+ export function buildSessionOptions(params: BuildOptionsParams): Options {
179
+ const options: Options = {
180
+ ...params.userProvidedOptions,
181
+ systemPrompt: params.systemPrompt ?? buildSystemPrompt(),
182
+ settingSources: ["user", "project", "local"],
183
+ stderr: (err) => params.logger.error(err),
184
+ cwd: params.cwd,
185
+ includePartialMessages: true,
186
+ allowDangerouslySkipPermissions: !IS_ROOT,
187
+ permissionMode: params.permissionMode,
188
+ canUseTool: params.canUseTool,
189
+ executable: "node",
190
+ mcpServers: buildMcpServers(
191
+ params.userProvidedOptions?.mcpServers,
192
+ params.mcpServers,
193
+ ),
194
+ env: buildEnvironment(),
195
+ hooks: buildHooks(params.userProvidedOptions?.hooks, params.onModeChange),
196
+ abortController: getAbortController(
197
+ params.userProvidedOptions?.abortController,
198
+ ),
199
+ ...(params.onProcessSpawned && {
200
+ spawnClaudeCodeProcess: buildSpawnWrapper(
201
+ params.sessionId ?? "unknown",
202
+ params.onProcessSpawned,
203
+ params.onProcessExited,
204
+ ),
205
+ }),
206
+ };
207
+
208
+ if (process.env.CLAUDE_CODE_EXECUTABLE) {
209
+ options.pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_EXECUTABLE;
210
+ }
211
+
212
+ if (params.sessionId) {
213
+ options.resume = params.sessionId;
214
+ }
215
+
216
+ if (params.additionalDirectories) {
217
+ options.additionalDirectories = params.additionalDirectories;
218
+ }
219
+
220
+ clearStatsigCache();
221
+ return options;
222
+ }
223
+
224
+ function clearStatsigCache(): void {
225
+ const statsigPath = path.join(
226
+ process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude"),
227
+ "statsig",
228
+ );
229
+ try {
230
+ if (fs.existsSync(statsigPath)) {
231
+ fs.rmSync(statsigPath, { recursive: true, force: true });
232
+ }
233
+ } catch {
234
+ // Ignore errors - cache clearing is best-effort
235
+ }
236
+ }
@@ -0,0 +1,143 @@
1
+ import { z } from "zod";
2
+
3
+ const QuestionOptionSchema = z.object({
4
+ label: z.string(),
5
+ description: z.string().optional(),
6
+ });
7
+
8
+ const QuestionItemSchema = z.object({
9
+ question: z.string(),
10
+ header: z.string().optional(),
11
+ options: z.array(QuestionOptionSchema),
12
+ multiSelect: z.boolean().optional(),
13
+ completed: z.boolean().optional(),
14
+ });
15
+
16
+ export const QuestionMetaSchema = z.object({
17
+ questions: z.array(QuestionItemSchema),
18
+ });
19
+
20
+ export type QuestionOption = z.infer<typeof QuestionOptionSchema>;
21
+ export type QuestionItem = z.infer<typeof QuestionItemSchema>;
22
+ export type QuestionMeta = z.infer<typeof QuestionMetaSchema>;
23
+
24
+ const toolSchemas = {
25
+ bash: z.object({
26
+ command: z.string(),
27
+ description: z.string().optional(),
28
+ }),
29
+ edit: z.object({
30
+ file_path: z.string(),
31
+ old_string: z.string(),
32
+ new_string: z.string(),
33
+ replace_all: z.boolean().optional(),
34
+ }),
35
+ write: z.object({
36
+ file_path: z.string(),
37
+ content: z.string(),
38
+ }),
39
+ read: z.object({
40
+ file_path: z.string(),
41
+ offset: z.number().optional(),
42
+ limit: z.number().optional(),
43
+ }),
44
+ switch_mode: z.object({
45
+ plan: z.string().optional(),
46
+ }),
47
+ } as const;
48
+
49
+ type ToolKind = keyof typeof toolSchemas;
50
+ const _toolKinds = Object.keys(toolSchemas) as ToolKind[];
51
+
52
+ const sdkToolNameToKind: Record<string, ToolKind> = {
53
+ Bash: "bash",
54
+ Edit: "edit",
55
+ Write: "write",
56
+ Read: "read",
57
+ ExitPlanMode: "switch_mode",
58
+ };
59
+
60
+ const BaseTwigToolMetaSchema = z.object({
61
+ claudeCode: z
62
+ .object({
63
+ toolName: z.string(),
64
+ toolResponse: z.unknown().optional(),
65
+ })
66
+ .optional(),
67
+ });
68
+
69
+ type BaseMeta = z.infer<typeof BaseTwigToolMetaSchema>;
70
+
71
+ type ToolMeta<K extends ToolKind> = BaseMeta & {
72
+ twigToolKind: K;
73
+ } & { [P in K]: z.infer<(typeof toolSchemas)[K]> };
74
+
75
+ export type BashToolMeta = ToolMeta<"bash">;
76
+ export type EditToolMeta = ToolMeta<"edit">;
77
+ export type WriteToolMeta = ToolMeta<"write">;
78
+ export type ReadToolMeta = ToolMeta<"read">;
79
+ export type SwitchModeToolMeta = ToolMeta<"switch_mode">;
80
+ export type QuestionToolMeta = BaseMeta & {
81
+ twigToolKind: "question";
82
+ questions: QuestionItem[];
83
+ };
84
+ export type GenericToolMeta = BaseMeta & { twigToolKind?: undefined };
85
+
86
+ export type TwigToolMeta =
87
+ | BashToolMeta
88
+ | EditToolMeta
89
+ | WriteToolMeta
90
+ | ReadToolMeta
91
+ | SwitchModeToolMeta
92
+ | QuestionToolMeta
93
+ | GenericToolMeta;
94
+
95
+ export function isBashToolMeta(meta: TwigToolMeta): meta is BashToolMeta {
96
+ return meta.twigToolKind === "bash";
97
+ }
98
+
99
+ export function isEditToolMeta(meta: TwigToolMeta): meta is EditToolMeta {
100
+ return meta.twigToolKind === "edit";
101
+ }
102
+
103
+ export function isWriteToolMeta(meta: TwigToolMeta): meta is WriteToolMeta {
104
+ return meta.twigToolKind === "write";
105
+ }
106
+
107
+ export function isReadToolMeta(meta: TwigToolMeta): meta is ReadToolMeta {
108
+ return meta.twigToolKind === "read";
109
+ }
110
+
111
+ export function isSwitchModeToolMeta(
112
+ meta: TwigToolMeta,
113
+ ): meta is SwitchModeToolMeta {
114
+ return meta.twigToolKind === "switch_mode";
115
+ }
116
+
117
+ export function isQuestionToolMeta(
118
+ meta: TwigToolMeta,
119
+ ): meta is QuestionToolMeta {
120
+ return meta.twigToolKind === "question";
121
+ }
122
+
123
+ export function buildToolMeta(
124
+ toolName: string,
125
+ input: Record<string, unknown>,
126
+ ): TwigToolMeta {
127
+ const kind = sdkToolNameToKind[toolName];
128
+ if (!kind) {
129
+ return { claudeCode: { toolName } };
130
+ }
131
+
132
+ const schema = toolSchemas[kind];
133
+ const result = schema.safeParse(input);
134
+ if (!result.success) {
135
+ return { claudeCode: { toolName } };
136
+ }
137
+
138
+ return {
139
+ claudeCode: { toolName },
140
+ twigToolKind: kind,
141
+ [kind]: result.data,
142
+ } as TwigToolMeta;
143
+ }