@clinebot/core 0.0.21 → 0.0.23

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 (260) hide show
  1. package/dist/ClineCore.d.ts +110 -0
  2. package/dist/ClineCore.d.ts.map +1 -0
  3. package/dist/account/cline-account-service.d.ts +2 -1
  4. package/dist/account/cline-account-service.d.ts.map +1 -1
  5. package/dist/account/index.d.ts +1 -1
  6. package/dist/account/index.d.ts.map +1 -1
  7. package/dist/account/rpc.d.ts +3 -1
  8. package/dist/account/rpc.d.ts.map +1 -1
  9. package/dist/account/types.d.ts +3 -0
  10. package/dist/account/types.d.ts.map +1 -1
  11. package/dist/agents/plugin-loader.d.ts.map +1 -1
  12. package/dist/agents/plugin-sandbox-bootstrap.js +17 -17
  13. package/dist/auth/client.d.ts +1 -1
  14. package/dist/auth/client.d.ts.map +1 -1
  15. package/dist/auth/cline.d.ts +1 -1
  16. package/dist/auth/cline.d.ts.map +1 -1
  17. package/dist/auth/codex.d.ts +1 -1
  18. package/dist/auth/codex.d.ts.map +1 -1
  19. package/dist/auth/oca.d.ts +1 -1
  20. package/dist/auth/oca.d.ts.map +1 -1
  21. package/dist/auth/utils.d.ts +2 -2
  22. package/dist/auth/utils.d.ts.map +1 -1
  23. package/dist/index.d.ts +50 -5
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +949 -0
  26. package/dist/providers/local-provider-service.d.ts +4 -4
  27. package/dist/providers/local-provider-service.d.ts.map +1 -1
  28. package/dist/runtime/runtime-builder.d.ts +1 -0
  29. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  30. package/dist/runtime/session-runtime.d.ts +2 -1
  31. package/dist/runtime/session-runtime.d.ts.map +1 -1
  32. package/dist/runtime/team-runtime-registry.d.ts +13 -0
  33. package/dist/runtime/team-runtime-registry.d.ts.map +1 -0
  34. package/dist/session/default-session-manager.d.ts +2 -2
  35. package/dist/session/default-session-manager.d.ts.map +1 -1
  36. package/dist/session/rpc-runtime-ensure.d.ts +53 -0
  37. package/dist/session/rpc-runtime-ensure.d.ts.map +1 -0
  38. package/dist/session/session-config-builder.d.ts +2 -3
  39. package/dist/session/session-config-builder.d.ts.map +1 -1
  40. package/dist/session/session-host.d.ts +8 -18
  41. package/dist/session/session-host.d.ts.map +1 -1
  42. package/dist/session/session-manager.d.ts +1 -1
  43. package/dist/session/session-manager.d.ts.map +1 -1
  44. package/dist/session/session-manifest.d.ts +1 -2
  45. package/dist/session/session-manifest.d.ts.map +1 -1
  46. package/dist/session/unified-session-persistence-service.d.ts +2 -2
  47. package/dist/session/unified-session-persistence-service.d.ts.map +1 -1
  48. package/dist/session/utils/helpers.d.ts +1 -1
  49. package/dist/session/utils/helpers.d.ts.map +1 -1
  50. package/dist/session/utils/types.d.ts +1 -1
  51. package/dist/session/utils/types.d.ts.map +1 -1
  52. package/dist/storage/provider-settings-legacy-migration.d.ts.map +1 -1
  53. package/dist/telemetry/OpenTelemetryProvider.d.ts.map +1 -1
  54. package/dist/telemetry/distinct-id.d.ts +2 -0
  55. package/dist/telemetry/distinct-id.d.ts.map +1 -0
  56. package/dist/telemetry/{opentelemetry.d.ts → index.d.ts} +1 -1
  57. package/dist/telemetry/index.d.ts.map +1 -0
  58. package/dist/telemetry/index.js +28 -0
  59. package/dist/tools/constants.d.ts +1 -1
  60. package/dist/tools/constants.d.ts.map +1 -1
  61. package/dist/tools/definitions.d.ts +3 -3
  62. package/dist/tools/definitions.d.ts.map +1 -1
  63. package/dist/tools/executors/apply-patch.d.ts +1 -1
  64. package/dist/tools/executors/apply-patch.d.ts.map +1 -1
  65. package/dist/tools/executors/bash.d.ts +1 -1
  66. package/dist/tools/executors/bash.d.ts.map +1 -1
  67. package/dist/tools/executors/editor.d.ts +1 -1
  68. package/dist/tools/executors/editor.d.ts.map +1 -1
  69. package/dist/tools/executors/file-read.d.ts +1 -1
  70. package/dist/tools/executors/file-read.d.ts.map +1 -1
  71. package/dist/tools/executors/index.d.ts +14 -14
  72. package/dist/tools/executors/index.d.ts.map +1 -1
  73. package/dist/tools/executors/search.d.ts +1 -1
  74. package/dist/tools/executors/search.d.ts.map +1 -1
  75. package/dist/tools/executors/web-fetch.d.ts +1 -1
  76. package/dist/tools/executors/web-fetch.d.ts.map +1 -1
  77. package/dist/tools/helpers.d.ts +1 -1
  78. package/dist/tools/helpers.d.ts.map +1 -1
  79. package/dist/tools/index.d.ts +10 -10
  80. package/dist/tools/index.d.ts.map +1 -1
  81. package/dist/tools/model-tool-routing.d.ts +1 -1
  82. package/dist/tools/model-tool-routing.d.ts.map +1 -1
  83. package/dist/tools/presets.d.ts +1 -1
  84. package/dist/tools/presets.d.ts.map +1 -1
  85. package/dist/types/common.d.ts +17 -8
  86. package/dist/types/common.d.ts.map +1 -1
  87. package/dist/types/config.d.ts +4 -3
  88. package/dist/types/config.d.ts.map +1 -1
  89. package/dist/types/provider-settings.d.ts +1 -1
  90. package/dist/types/provider-settings.d.ts.map +1 -1
  91. package/dist/types.d.ts +5 -2
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/version.d.ts +2 -0
  94. package/dist/version.d.ts.map +1 -0
  95. package/package.json +44 -38
  96. package/src/ClineCore.ts +137 -0
  97. package/src/account/cline-account-service.test.ts +101 -0
  98. package/src/account/cline-account-service.ts +300 -0
  99. package/src/account/featurebase-token.test.ts +175 -0
  100. package/src/account/index.ts +23 -0
  101. package/src/account/rpc.test.ts +63 -0
  102. package/src/account/rpc.ts +185 -0
  103. package/src/account/types.ts +102 -0
  104. package/src/agents/agent-config-loader.test.ts +236 -0
  105. package/src/agents/agent-config-loader.ts +108 -0
  106. package/src/agents/agent-config-parser.ts +198 -0
  107. package/src/agents/hooks-config-loader.test.ts +20 -0
  108. package/src/agents/hooks-config-loader.ts +118 -0
  109. package/src/agents/index.ts +85 -0
  110. package/src/agents/plugin-config-loader.test.ts +140 -0
  111. package/src/agents/plugin-config-loader.ts +97 -0
  112. package/src/agents/plugin-loader.test.ts +210 -0
  113. package/src/agents/plugin-loader.ts +175 -0
  114. package/src/agents/plugin-sandbox-bootstrap.ts +448 -0
  115. package/src/agents/plugin-sandbox.test.ts +296 -0
  116. package/src/agents/plugin-sandbox.ts +341 -0
  117. package/src/agents/unified-config-file-watcher.test.ts +196 -0
  118. package/src/agents/unified-config-file-watcher.ts +483 -0
  119. package/src/agents/user-instruction-config-loader.test.ts +158 -0
  120. package/src/agents/user-instruction-config-loader.ts +438 -0
  121. package/src/auth/client.test.ts +40 -0
  122. package/src/auth/client.ts +25 -0
  123. package/src/auth/cline.test.ts +130 -0
  124. package/src/auth/cline.ts +420 -0
  125. package/src/auth/codex.test.ts +170 -0
  126. package/src/auth/codex.ts +491 -0
  127. package/src/auth/oca.test.ts +215 -0
  128. package/src/auth/oca.ts +573 -0
  129. package/src/auth/server.ts +216 -0
  130. package/src/auth/types.ts +81 -0
  131. package/src/auth/utils.test.ts +128 -0
  132. package/src/auth/utils.ts +247 -0
  133. package/src/chat/chat-schema.ts +82 -0
  134. package/src/index.ts +479 -0
  135. package/src/input/file-indexer.d.ts +11 -0
  136. package/src/input/file-indexer.test.ts +127 -0
  137. package/src/input/file-indexer.ts +327 -0
  138. package/src/input/index.ts +7 -0
  139. package/src/input/mention-enricher.test.ts +85 -0
  140. package/src/input/mention-enricher.ts +122 -0
  141. package/src/mcp/config-loader.test.ts +238 -0
  142. package/src/mcp/config-loader.ts +219 -0
  143. package/src/mcp/index.ts +26 -0
  144. package/src/mcp/manager.test.ts +106 -0
  145. package/src/mcp/manager.ts +262 -0
  146. package/src/mcp/types.ts +88 -0
  147. package/src/providers/local-provider-registry.ts +232 -0
  148. package/src/providers/local-provider-service.test.ts +783 -0
  149. package/src/providers/local-provider-service.ts +471 -0
  150. package/src/runtime/commands.test.ts +98 -0
  151. package/src/runtime/commands.ts +83 -0
  152. package/src/runtime/hook-file-hooks.test.ts +237 -0
  153. package/src/runtime/hook-file-hooks.ts +859 -0
  154. package/src/runtime/index.ts +37 -0
  155. package/src/runtime/rules.ts +34 -0
  156. package/src/runtime/runtime-builder.team-persistence.test.ts +245 -0
  157. package/src/runtime/runtime-builder.test.ts +371 -0
  158. package/src/runtime/runtime-builder.ts +631 -0
  159. package/src/runtime/runtime-parity.test.ts +143 -0
  160. package/src/runtime/sandbox/subprocess-sandbox.ts +231 -0
  161. package/src/runtime/session-runtime.ts +49 -0
  162. package/src/runtime/skills.ts +44 -0
  163. package/src/runtime/team-runtime-registry.ts +46 -0
  164. package/src/runtime/tool-approval.ts +104 -0
  165. package/src/runtime/workflows.test.ts +119 -0
  166. package/src/runtime/workflows.ts +45 -0
  167. package/src/session/default-session-manager.e2e.test.ts +384 -0
  168. package/src/session/default-session-manager.test.ts +1931 -0
  169. package/src/session/default-session-manager.ts +1422 -0
  170. package/src/session/file-session-service.ts +280 -0
  171. package/src/session/index.ts +45 -0
  172. package/src/session/rpc-runtime-ensure.ts +521 -0
  173. package/src/session/rpc-session-service.ts +107 -0
  174. package/src/session/rpc-spawn-lease.test.ts +49 -0
  175. package/src/session/rpc-spawn-lease.ts +122 -0
  176. package/src/session/runtime-oauth-token-manager.test.ts +137 -0
  177. package/src/session/runtime-oauth-token-manager.ts +272 -0
  178. package/src/session/session-agent-events.ts +248 -0
  179. package/src/session/session-artifacts.ts +106 -0
  180. package/src/session/session-config-builder.ts +113 -0
  181. package/src/session/session-graph.ts +92 -0
  182. package/src/session/session-host.test.ts +89 -0
  183. package/src/session/session-host.ts +205 -0
  184. package/src/session/session-manager.ts +69 -0
  185. package/src/session/session-manifest.ts +29 -0
  186. package/src/session/session-service.team-persistence.test.ts +48 -0
  187. package/src/session/session-service.ts +673 -0
  188. package/src/session/session-team-coordination.ts +229 -0
  189. package/src/session/session-telemetry.ts +100 -0
  190. package/src/session/sqlite-rpc-session-backend.ts +303 -0
  191. package/src/session/unified-session-persistence-service.test.ts +85 -0
  192. package/src/session/unified-session-persistence-service.ts +994 -0
  193. package/src/session/utils/helpers.ts +139 -0
  194. package/src/session/utils/types.ts +57 -0
  195. package/src/session/utils/usage.ts +32 -0
  196. package/src/session/workspace-manager.ts +98 -0
  197. package/src/session/workspace-manifest.ts +100 -0
  198. package/src/storage/artifact-store.ts +1 -0
  199. package/src/storage/file-team-store.ts +257 -0
  200. package/src/storage/index.ts +11 -0
  201. package/src/storage/provider-settings-legacy-migration.test.ts +424 -0
  202. package/src/storage/provider-settings-legacy-migration.ts +826 -0
  203. package/src/storage/provider-settings-manager.test.ts +191 -0
  204. package/src/storage/provider-settings-manager.ts +152 -0
  205. package/src/storage/session-store.ts +1 -0
  206. package/src/storage/sqlite-session-store.ts +275 -0
  207. package/src/storage/sqlite-team-store.ts +454 -0
  208. package/src/storage/team-store.ts +40 -0
  209. package/src/team/index.ts +4 -0
  210. package/src/team/projections.ts +285 -0
  211. package/src/telemetry/ITelemetryAdapter.ts +94 -0
  212. package/src/telemetry/LoggerTelemetryAdapter.test.ts +42 -0
  213. package/src/telemetry/LoggerTelemetryAdapter.ts +114 -0
  214. package/src/telemetry/OpenTelemetryAdapter.test.ts +157 -0
  215. package/src/telemetry/OpenTelemetryAdapter.ts +348 -0
  216. package/src/telemetry/OpenTelemetryProvider.test.ts +113 -0
  217. package/src/telemetry/OpenTelemetryProvider.ts +325 -0
  218. package/src/telemetry/TelemetryService.test.ts +134 -0
  219. package/src/telemetry/TelemetryService.ts +141 -0
  220. package/src/telemetry/core-events.ts +400 -0
  221. package/src/telemetry/distinct-id.test.ts +57 -0
  222. package/src/telemetry/distinct-id.ts +58 -0
  223. package/src/telemetry/index.ts +20 -0
  224. package/src/tools/constants.ts +35 -0
  225. package/src/tools/definitions.test.ts +704 -0
  226. package/src/tools/definitions.ts +709 -0
  227. package/src/tools/executors/apply-patch-parser.ts +520 -0
  228. package/src/tools/executors/apply-patch.ts +359 -0
  229. package/src/tools/executors/bash.test.ts +87 -0
  230. package/src/tools/executors/bash.ts +207 -0
  231. package/src/tools/executors/editor.test.ts +35 -0
  232. package/src/tools/executors/editor.ts +219 -0
  233. package/src/tools/executors/file-read.test.ts +49 -0
  234. package/src/tools/executors/file-read.ts +110 -0
  235. package/src/tools/executors/index.ts +87 -0
  236. package/src/tools/executors/search.ts +278 -0
  237. package/src/tools/executors/web-fetch.ts +259 -0
  238. package/src/tools/helpers.ts +130 -0
  239. package/src/tools/index.ts +169 -0
  240. package/src/tools/model-tool-routing.test.ts +86 -0
  241. package/src/tools/model-tool-routing.ts +132 -0
  242. package/src/tools/presets.test.ts +62 -0
  243. package/src/tools/presets.ts +168 -0
  244. package/src/tools/schemas.ts +327 -0
  245. package/src/tools/types.ts +329 -0
  246. package/src/types/common.ts +26 -0
  247. package/src/types/config.ts +86 -0
  248. package/src/types/events.ts +74 -0
  249. package/src/types/index.ts +24 -0
  250. package/src/types/provider-settings.ts +43 -0
  251. package/src/types/sessions.ts +16 -0
  252. package/src/types/storage.ts +64 -0
  253. package/src/types/workspace.ts +7 -0
  254. package/src/types.ts +132 -0
  255. package/src/version.ts +3 -0
  256. package/dist/index.node.d.ts +0 -47
  257. package/dist/index.node.d.ts.map +0 -1
  258. package/dist/index.node.js +0 -948
  259. package/dist/telemetry/opentelemetry.d.ts.map +0 -1
  260. package/dist/telemetry/opentelemetry.js +0 -27
@@ -0,0 +1,859 @@
1
+ import { spawn } from "node:child_process";
2
+ import { appendFileSync, readFileSync } from "node:fs";
3
+ import type {
4
+ AgentHooks,
5
+ HookEventName,
6
+ HookEventPayload,
7
+ } from "@clinebot/agents";
8
+ import type { BasicLogger, HookSessionContext } from "@clinebot/shared";
9
+ import { ensureParentDir } from "@clinebot/shared/storage";
10
+ import { listHookConfigFiles } from "../agents/hooks-config-loader";
11
+
12
+ type HookContextBase = {
13
+ agentId: string;
14
+ conversationId: string;
15
+ parentAgentId: string | null;
16
+ };
17
+
18
+ type AgentHookControl = NonNullable<
19
+ Awaited<ReturnType<NonNullable<AgentHooks["onToolCallStart"]>>>
20
+ >;
21
+ type AgentHookRunStartContext = Parameters<
22
+ NonNullable<AgentHooks["onRunStart"]>
23
+ >[0];
24
+ type AgentHookToolCallStartContext = Parameters<
25
+ NonNullable<AgentHooks["onToolCallStart"]>
26
+ >[0];
27
+ type AgentHookToolCallEndContext = Parameters<
28
+ NonNullable<AgentHooks["onToolCallEnd"]>
29
+ >[0];
30
+ type AgentHookTurnEndContext = Parameters<
31
+ NonNullable<AgentHooks["onTurnEnd"]>
32
+ >[0];
33
+ type AgentHookStopErrorContext = Parameters<
34
+ NonNullable<AgentHooks["onStopError"]>
35
+ >[0];
36
+ type AgentHookSessionShutdownContext = Parameters<
37
+ NonNullable<AgentHooks["onSessionShutdown"]>
38
+ >[0];
39
+
40
+ type HookRuntimeOptions = {
41
+ cwd: string;
42
+ workspacePath: string;
43
+ hookLogPath?: string;
44
+ rootSessionId?: string;
45
+ logger?: BasicLogger;
46
+ toolCallTimeoutMs?: number;
47
+ };
48
+
49
+ function mapParams(input: unknown): Record<string, string> {
50
+ if (!input || typeof input !== "object") {
51
+ return {};
52
+ }
53
+ const output: Record<string, string> = {};
54
+ for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
55
+ output[key] = typeof value === "string" ? value : JSON.stringify(value);
56
+ }
57
+ return output;
58
+ }
59
+
60
+ function logHookError(
61
+ logger: BasicLogger | undefined,
62
+ message: string,
63
+ error?: unknown,
64
+ ): void {
65
+ const detail = error instanceof Error ? `: ${error.message}` : "";
66
+ const text = `${message}${detail}`;
67
+ if (logger?.warn) {
68
+ logger.warn(text);
69
+ return;
70
+ }
71
+ console.warn(text);
72
+ }
73
+
74
+ function mergeHookControls(
75
+ current: AgentHookControl | undefined,
76
+ next: AgentHookControl | undefined,
77
+ ): AgentHookControl | undefined {
78
+ if (!next) {
79
+ return current;
80
+ }
81
+ if (!current) {
82
+ return { ...next };
83
+ }
84
+ const contexts = [current.context, next.context]
85
+ .filter(
86
+ (value): value is string => typeof value === "string" && value.length > 0,
87
+ )
88
+ .join("\n");
89
+ const appendMessages = [
90
+ ...(current.appendMessages ?? []),
91
+ ...(next.appendMessages ?? []),
92
+ ];
93
+ return {
94
+ cancel: current.cancel === true || next.cancel === true ? true : undefined,
95
+ review: current.review === true || next.review === true ? true : undefined,
96
+ context: contexts || undefined,
97
+ overrideInput:
98
+ next.overrideInput !== undefined
99
+ ? next.overrideInput
100
+ : current.overrideInput,
101
+ systemPrompt:
102
+ next.systemPrompt !== undefined
103
+ ? next.systemPrompt
104
+ : current.systemPrompt,
105
+ appendMessages: appendMessages.length > 0 ? appendMessages : undefined,
106
+ };
107
+ }
108
+
109
+ function parseHookControl(value: unknown): AgentHookControl | undefined {
110
+ if (!value || typeof value !== "object") {
111
+ return undefined;
112
+ }
113
+ const record = value as Record<string, unknown>;
114
+ const context =
115
+ typeof record.context === "string"
116
+ ? record.context
117
+ : typeof record.contextModification === "string"
118
+ ? record.contextModification
119
+ : typeof record.errorMessage === "string"
120
+ ? record.errorMessage
121
+ : undefined;
122
+ return {
123
+ cancel: typeof record.cancel === "boolean" ? record.cancel : undefined,
124
+ review: typeof record.review === "boolean" ? record.review : undefined,
125
+ context,
126
+ overrideInput: Object.hasOwn(record, "overrideInput")
127
+ ? record.overrideInput
128
+ : undefined,
129
+ };
130
+ }
131
+
132
+ function isAbortReason(reason?: string): boolean {
133
+ const value = String(reason ?? "").toLowerCase();
134
+ return (
135
+ value.includes("cancel") ||
136
+ value.includes("abort") ||
137
+ value.includes("interrupt")
138
+ );
139
+ }
140
+
141
+ function ensureHookLogDir(filePath: string): void {
142
+ ensureParentDir(filePath);
143
+ }
144
+
145
+ function createPayloadBase(
146
+ ctx: HookContextBase,
147
+ options: HookRuntimeOptions,
148
+ ): Omit<HookEventPayload, "hookName"> {
149
+ const userId =
150
+ process.env.CLINE_USER_ID?.trim() || process.env.USER?.trim() || "unknown";
151
+ const sessionContext: HookSessionContext = {
152
+ rootSessionId: options.rootSessionId || ctx.conversationId,
153
+ hookLogPath: options.hookLogPath,
154
+ };
155
+ return {
156
+ clineVersion: process.env.CLINE_VERSION?.trim() || "",
157
+ timestamp: new Date().toISOString(),
158
+ taskId: ctx.conversationId,
159
+ sessionContext,
160
+ workspaceRoots: options.workspacePath ? [options.workspacePath] : [],
161
+ userId,
162
+ agent_id: ctx.agentId,
163
+ parent_agent_id: ctx.parentAgentId,
164
+ } as Omit<HookEventPayload, "hookName">;
165
+ }
166
+
167
+ type HookCommandMap = Partial<Record<HookEventName, string[][]>>;
168
+
169
+ interface HookCommandResult {
170
+ exitCode: number | null;
171
+ stdout: string;
172
+ stderr: string;
173
+ parsedJson?: unknown;
174
+ parseError?: string;
175
+ timedOut?: boolean;
176
+ }
177
+
178
+ function parseHookStdout(stdout: string): {
179
+ parsedJson?: unknown;
180
+ parseError?: string;
181
+ } {
182
+ const trimmed = stdout.trim();
183
+ if (!trimmed) {
184
+ return {};
185
+ }
186
+ const lines = trimmed
187
+ .split("\n")
188
+ .map((line) => line.trim())
189
+ .filter(Boolean);
190
+ const prefixed = lines
191
+ .filter((line) => line.startsWith("HOOK_CONTROL\t"))
192
+ .map((line) => line.slice("HOOK_CONTROL\t".length));
193
+ const candidate =
194
+ prefixed.length > 0 ? prefixed[prefixed.length - 1] : trimmed;
195
+ try {
196
+ return { parsedJson: JSON.parse(candidate) };
197
+ } catch (error) {
198
+ return {
199
+ parseError:
200
+ error instanceof Error
201
+ ? error.message
202
+ : "Failed to parse hook stdout JSON",
203
+ };
204
+ }
205
+ }
206
+
207
+ async function writeToChildStdin(
208
+ child: ReturnType<typeof spawn>,
209
+ body: string,
210
+ ): Promise<void> {
211
+ const stdin = child.stdin;
212
+ if (!stdin) {
213
+ throw new Error("hook command failed to create stdin");
214
+ }
215
+
216
+ await new Promise<void>((resolve, reject) => {
217
+ let settled = false;
218
+ const cleanup = () => {
219
+ stdin.off("error", onError);
220
+ stdin.off("finish", onFinish);
221
+ child.off("close", onChildClose);
222
+ };
223
+ const finish = (error?: Error | null) => {
224
+ if (settled) {
225
+ return;
226
+ }
227
+ settled = true;
228
+ cleanup();
229
+ if (error) {
230
+ const code = (error as Error & { code?: string }).code;
231
+ if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED") {
232
+ resolve();
233
+ return;
234
+ }
235
+ reject(error);
236
+ return;
237
+ }
238
+ resolve();
239
+ };
240
+ const onError = (error: Error) => finish(error);
241
+ const onFinish = () => finish();
242
+ const onChildClose = () => finish();
243
+ stdin.on("error", onError);
244
+ stdin.once("finish", onFinish);
245
+ child.once("close", onChildClose);
246
+ try {
247
+ stdin.end(body);
248
+ } catch (error) {
249
+ finish(error as Error);
250
+ }
251
+ });
252
+ }
253
+
254
+ async function runHookCommand(
255
+ payload: HookEventPayload,
256
+ options: {
257
+ command: string[];
258
+ cwd: string;
259
+ env?: NodeJS.ProcessEnv;
260
+ detached: boolean;
261
+ timeoutMs?: number;
262
+ },
263
+ ): Promise<HookCommandResult | undefined> {
264
+ if (options.command.length === 0) {
265
+ throw new Error("runHookCommand requires non-empty command");
266
+ }
267
+ const child = spawn(options.command[0], options.command.slice(1), {
268
+ cwd: options.cwd,
269
+ env: options.env,
270
+ stdio: options.detached
271
+ ? ["pipe", "ignore", "ignore"]
272
+ : ["pipe", "pipe", "pipe"],
273
+ detached: options.detached,
274
+ });
275
+ const spawned = new Promise<void>((resolve) => {
276
+ child.once("spawn", () => resolve());
277
+ });
278
+ const childError = new Promise<never>((_, reject) => {
279
+ child.once("error", (error) => reject(error));
280
+ });
281
+
282
+ const body = JSON.stringify(payload);
283
+ await Promise.race([spawned, childError]);
284
+ await writeToChildStdin(child, body);
285
+
286
+ if (options.detached) {
287
+ child.unref();
288
+ return;
289
+ }
290
+
291
+ if (!child.stdout || !child.stderr) {
292
+ throw new Error("hook command failed to create stdout/stderr");
293
+ }
294
+ let stdout = "";
295
+ let stderr = "";
296
+ let timedOut = false;
297
+ let timeoutId: NodeJS.Timeout | undefined;
298
+ child.stdout.on("data", (chunk: Buffer | string) => {
299
+ stdout += chunk.toString();
300
+ });
301
+ child.stderr.on("data", (chunk: Buffer | string) => {
302
+ stderr += chunk.toString();
303
+ });
304
+
305
+ const result = new Promise<HookCommandResult>((resolve) => {
306
+ if ((options.timeoutMs ?? 0) > 0) {
307
+ timeoutId = setTimeout(() => {
308
+ timedOut = true;
309
+ child.kill("SIGKILL");
310
+ }, options.timeoutMs);
311
+ }
312
+ child.once("close", (exitCode) => {
313
+ if (timeoutId) {
314
+ clearTimeout(timeoutId);
315
+ }
316
+ const { parsedJson, parseError } = parseHookStdout(stdout);
317
+ resolve({
318
+ exitCode,
319
+ stdout,
320
+ stderr,
321
+ parsedJson,
322
+ parseError,
323
+ timedOut,
324
+ });
325
+ });
326
+ });
327
+ return await Promise.race([result, childError]);
328
+ }
329
+
330
+ function parseShebangCommand(path: string): string[] | undefined {
331
+ try {
332
+ const content = readFileSync(path, "utf8");
333
+ const firstLine = content.split(/\r?\n/, 1)[0]?.trim();
334
+ if (!firstLine?.startsWith("#!")) {
335
+ return undefined;
336
+ }
337
+ const shebang = firstLine.slice(2).trim();
338
+ if (!shebang) {
339
+ return undefined;
340
+ }
341
+ const tokens = shebang.split(/\s+/).filter(Boolean);
342
+ return tokens.length > 0 ? tokens : undefined;
343
+ } catch {
344
+ return undefined;
345
+ }
346
+ }
347
+
348
+ function normalizeHookInterpreter(tokens: string[]): string[] | undefined {
349
+ if (tokens.length === 0) {
350
+ return undefined;
351
+ }
352
+ const [rawCommand, ...rest] = tokens;
353
+ const normalizedCommand = rawCommand.replace(/\\/g, "/").toLowerCase();
354
+ const commandName = normalizedCommand.split("/").at(-1) ?? normalizedCommand;
355
+
356
+ if (commandName === "env") {
357
+ return normalizeHookInterpreter(rest);
358
+ }
359
+
360
+ if (commandName === "bash" || commandName === "sh" || commandName === "zsh") {
361
+ return [commandName, ...rest];
362
+ }
363
+
364
+ if (commandName === "python3" || commandName === "python") {
365
+ return [process.platform === "win32" ? "python" : commandName, ...rest];
366
+ }
367
+
368
+ return tokens;
369
+ }
370
+
371
+ function inferHookCommand(path: string): string[] {
372
+ const shebang = parseShebangCommand(path);
373
+ if (shebang && shebang.length > 0) {
374
+ return [...(normalizeHookInterpreter(shebang) ?? shebang), path];
375
+ }
376
+ const lowered = path.toLowerCase();
377
+ if (
378
+ lowered.endsWith(".sh") ||
379
+ lowered.endsWith(".bash") ||
380
+ lowered.endsWith(".zsh")
381
+ ) {
382
+ return ["bash", path];
383
+ }
384
+ if (
385
+ lowered.endsWith(".js") ||
386
+ lowered.endsWith(".mjs") ||
387
+ lowered.endsWith(".cjs")
388
+ ) {
389
+ return ["node", path];
390
+ }
391
+ if (
392
+ lowered.endsWith(".ts") ||
393
+ lowered.endsWith(".mts") ||
394
+ lowered.endsWith(".cts")
395
+ ) {
396
+ return ["bun", "run", path];
397
+ }
398
+ if (lowered.endsWith(".py")) {
399
+ return [process.platform === "win32" ? "python" : "python3", path];
400
+ }
401
+ if (lowered.endsWith(".ps1")) {
402
+ return [
403
+ process.platform === "win32" ? "powershell" : "pwsh",
404
+ "-File",
405
+ path,
406
+ ];
407
+ }
408
+ // Default to bash for legacy hook files with no extension/shebang.
409
+ return ["bash", path];
410
+ }
411
+
412
+ function createHookCommandMap(workspacePath: string): HookCommandMap {
413
+ const map: HookCommandMap = {};
414
+ for (const file of listHookConfigFiles(workspacePath)) {
415
+ if (!file.hookEventName) {
416
+ continue;
417
+ }
418
+ const existing = map[file.hookEventName] ?? [];
419
+ existing.push(inferHookCommand(file.path));
420
+ map[file.hookEventName] = existing;
421
+ }
422
+ return map;
423
+ }
424
+
425
+ async function runBlockingHookCommands(options: {
426
+ commands: string[][];
427
+ payload: HookEventPayload;
428
+ cwd: string;
429
+ logger?: BasicLogger;
430
+ timeoutMs?: number;
431
+ }): Promise<AgentHookControl | undefined> {
432
+ let merged: AgentHookControl | undefined;
433
+ for (const command of options.commands) {
434
+ const commandLabel = command.join(" ");
435
+ try {
436
+ const result = await runHookCommand(options.payload, {
437
+ command,
438
+ cwd: options.cwd,
439
+ env: process.env,
440
+ detached: false,
441
+ timeoutMs: options.timeoutMs,
442
+ });
443
+ if (result?.timedOut) {
444
+ logHookError(options.logger, `hook command timed out: ${commandLabel}`);
445
+ continue;
446
+ }
447
+ if (result?.parseError) {
448
+ logHookError(
449
+ options.logger,
450
+ `hook command returned invalid JSON control output: ${commandLabel} (${result.parseError})`,
451
+ );
452
+ continue;
453
+ }
454
+ merged = mergeHookControls(merged, parseHookControl(result?.parsedJson));
455
+ } catch (error) {
456
+ logHookError(
457
+ options.logger,
458
+ `hook command failed: ${commandLabel}`,
459
+ error,
460
+ );
461
+ }
462
+ }
463
+ return merged;
464
+ }
465
+
466
+ function runAsyncHookCommands(options: {
467
+ commands: string[][];
468
+ payload: HookEventPayload;
469
+ cwd: string;
470
+ logger?: BasicLogger;
471
+ }): void {
472
+ for (const command of options.commands) {
473
+ const commandLabel = command.join(" ");
474
+ void runHookCommand(options.payload, {
475
+ command,
476
+ cwd: options.cwd,
477
+ env: process.env,
478
+ detached: true,
479
+ }).catch((error) => {
480
+ logHookError(
481
+ options.logger,
482
+ `hook command failed: ${commandLabel}`,
483
+ error,
484
+ );
485
+ });
486
+ }
487
+ }
488
+
489
+ export function createHookAuditHooks(options: {
490
+ hookLogPath: string;
491
+ rootSessionId?: string;
492
+ workspacePath: string;
493
+ }): AgentHooks {
494
+ const runtimeOptions: HookRuntimeOptions = {
495
+ cwd: options.workspacePath,
496
+ workspacePath: options.workspacePath,
497
+ hookLogPath: options.hookLogPath,
498
+ rootSessionId: options.rootSessionId,
499
+ };
500
+
501
+ const append = (payload: HookEventPayload): void => {
502
+ const line = `${JSON.stringify({
503
+ ts: new Date().toISOString(),
504
+ ...payload,
505
+ })}\n`;
506
+ ensureHookLogDir(options.hookLogPath);
507
+ appendFileSync(options.hookLogPath, line, "utf8");
508
+ };
509
+
510
+ return {
511
+ onRunStart: async (ctx: AgentHookRunStartContext) => {
512
+ append({
513
+ ...createPayloadBase(ctx, runtimeOptions),
514
+ hookName: "agent_start",
515
+ taskStart: { taskMetadata: {} },
516
+ });
517
+ append({
518
+ ...createPayloadBase(ctx, runtimeOptions),
519
+ hookName: "prompt_submit",
520
+ userPromptSubmit: {
521
+ prompt: ctx.userMessage,
522
+ attachments: [],
523
+ },
524
+ });
525
+ return undefined;
526
+ },
527
+ onToolCallStart: async (ctx: AgentHookToolCallStartContext) => {
528
+ append({
529
+ ...createPayloadBase(ctx, runtimeOptions),
530
+ hookName: "tool_call",
531
+ iteration: ctx.iteration,
532
+ tool_call: {
533
+ id: ctx.call.id,
534
+ name: ctx.call.name,
535
+ input: ctx.call.input,
536
+ },
537
+ preToolUse: {
538
+ toolName: ctx.call.name,
539
+ parameters: mapParams(ctx.call.input),
540
+ },
541
+ });
542
+ return undefined;
543
+ },
544
+ onToolCallEnd: async (ctx: AgentHookToolCallEndContext) => {
545
+ append({
546
+ ...createPayloadBase(ctx, runtimeOptions),
547
+ hookName: "tool_result",
548
+ iteration: ctx.iteration,
549
+ tool_result: ctx.record,
550
+ postToolUse: {
551
+ toolName: ctx.record.name,
552
+ parameters: mapParams(ctx.record.input),
553
+ result:
554
+ typeof ctx.record.output === "string"
555
+ ? ctx.record.output
556
+ : JSON.stringify(ctx.record.output),
557
+ success: !ctx.record.error,
558
+ executionTimeMs: ctx.record.durationMs,
559
+ },
560
+ });
561
+ return undefined;
562
+ },
563
+ onTurnEnd: async (ctx: AgentHookTurnEndContext) => {
564
+ append({
565
+ ...createPayloadBase(ctx, runtimeOptions),
566
+ hookName: "agent_end",
567
+ iteration: ctx.iteration,
568
+ turn: ctx.turn,
569
+ taskComplete: { taskMetadata: {} },
570
+ });
571
+ return undefined;
572
+ },
573
+ onStopError: async (ctx: AgentHookStopErrorContext) => {
574
+ append({
575
+ ...createPayloadBase(ctx, runtimeOptions),
576
+ hookName: "agent_error",
577
+ iteration: ctx.iteration,
578
+ error: {
579
+ name: ctx.error.name,
580
+ message: ctx.error.message,
581
+ stack: ctx.error.stack,
582
+ },
583
+ });
584
+ return undefined;
585
+ },
586
+ onSessionShutdown: async (ctx: AgentHookSessionShutdownContext) => {
587
+ if (isAbortReason(ctx.reason)) {
588
+ append({
589
+ ...createPayloadBase(ctx, runtimeOptions),
590
+ hookName: "agent_abort",
591
+ reason: ctx.reason,
592
+ taskCancel: { taskMetadata: {} },
593
+ });
594
+ }
595
+ append({
596
+ ...createPayloadBase(ctx, runtimeOptions),
597
+ hookName: "session_shutdown",
598
+ reason: ctx.reason,
599
+ });
600
+ return undefined;
601
+ },
602
+ };
603
+ }
604
+
605
+ export function createHookConfigFileHooks(
606
+ options: HookRuntimeOptions,
607
+ ): AgentHooks | undefined {
608
+ const commandMap = createHookCommandMap(options.workspacePath);
609
+ const hasAnyHooks = Object.values(commandMap).some(
610
+ (paths) => (paths?.length ?? 0) > 0,
611
+ );
612
+ if (!hasAnyHooks) {
613
+ return undefined;
614
+ }
615
+
616
+ const runStartPayload = async (
617
+ ctx: AgentHookRunStartContext,
618
+ ): Promise<void> => {
619
+ const agentStart = commandMap.agent_start ?? [];
620
+ if (agentStart.length > 0) {
621
+ runAsyncHookCommands({
622
+ commands: agentStart,
623
+ cwd: options.cwd,
624
+ logger: options.logger,
625
+ payload: {
626
+ ...createPayloadBase(ctx, options),
627
+ hookName: "agent_start",
628
+ taskStart: { taskMetadata: {} },
629
+ },
630
+ });
631
+ }
632
+
633
+ const promptSubmit = commandMap.prompt_submit ?? [];
634
+ if (promptSubmit.length > 0) {
635
+ runAsyncHookCommands({
636
+ commands: promptSubmit,
637
+ cwd: options.cwd,
638
+ logger: options.logger,
639
+ payload: {
640
+ ...createPayloadBase(ctx, options),
641
+ hookName: "prompt_submit",
642
+ userPromptSubmit: {
643
+ prompt: ctx.userMessage,
644
+ attachments: [],
645
+ },
646
+ },
647
+ });
648
+ }
649
+ };
650
+
651
+ const runToolCallStart = async (
652
+ ctx: AgentHookToolCallStartContext,
653
+ ): Promise<AgentHookControl | undefined> => {
654
+ const commandPaths = commandMap.tool_call ?? [];
655
+ if (commandPaths.length === 0) {
656
+ return undefined;
657
+ }
658
+ return runBlockingHookCommands({
659
+ commands: commandPaths,
660
+ cwd: options.cwd,
661
+ logger: options.logger,
662
+ timeoutMs: options.toolCallTimeoutMs ?? 120000,
663
+ payload: {
664
+ ...createPayloadBase(ctx, options),
665
+ hookName: "tool_call",
666
+ iteration: ctx.iteration,
667
+ tool_call: {
668
+ id: ctx.call.id,
669
+ name: ctx.call.name,
670
+ input: ctx.call.input,
671
+ },
672
+ preToolUse: {
673
+ toolName: ctx.call.name,
674
+ parameters: mapParams(ctx.call.input),
675
+ },
676
+ },
677
+ });
678
+ };
679
+
680
+ const runToolCallEnd = async (
681
+ ctx: AgentHookToolCallEndContext,
682
+ ): Promise<void> => {
683
+ const commandPaths = commandMap.tool_result ?? [];
684
+ if (commandPaths.length === 0) {
685
+ return;
686
+ }
687
+ runAsyncHookCommands({
688
+ commands: commandPaths,
689
+ cwd: options.cwd,
690
+ logger: options.logger,
691
+ payload: {
692
+ ...createPayloadBase(ctx, options),
693
+ hookName: "tool_result",
694
+ iteration: ctx.iteration,
695
+ tool_result: ctx.record,
696
+ postToolUse: {
697
+ toolName: ctx.record.name,
698
+ parameters: mapParams(ctx.record.input),
699
+ result:
700
+ typeof ctx.record.output === "string"
701
+ ? ctx.record.output
702
+ : JSON.stringify(ctx.record.output),
703
+ success: !ctx.record.error,
704
+ executionTimeMs: ctx.record.durationMs,
705
+ },
706
+ },
707
+ });
708
+ };
709
+
710
+ const runTurnEnd = async (ctx: AgentHookTurnEndContext): Promise<void> => {
711
+ const commandPaths = commandMap.agent_end ?? [];
712
+ if (commandPaths.length === 0) {
713
+ return;
714
+ }
715
+ runAsyncHookCommands({
716
+ commands: commandPaths,
717
+ cwd: options.cwd,
718
+ logger: options.logger,
719
+ payload: {
720
+ ...createPayloadBase(ctx, options),
721
+ hookName: "agent_end",
722
+ iteration: ctx.iteration,
723
+ turn: ctx.turn,
724
+ taskComplete: { taskMetadata: {} },
725
+ },
726
+ });
727
+ };
728
+
729
+ const runStopError = async (
730
+ ctx: AgentHookStopErrorContext,
731
+ ): Promise<void> => {
732
+ const commandPaths = commandMap.agent_error ?? [];
733
+ if (commandPaths.length === 0) {
734
+ return;
735
+ }
736
+ runAsyncHookCommands({
737
+ commands: commandPaths,
738
+ cwd: options.cwd,
739
+ logger: options.logger,
740
+ payload: {
741
+ ...createPayloadBase(ctx, options),
742
+ hookName: "agent_error",
743
+ iteration: ctx.iteration,
744
+ error: {
745
+ name: ctx.error.name,
746
+ message: ctx.error.message,
747
+ stack: ctx.error.stack,
748
+ },
749
+ },
750
+ });
751
+ };
752
+
753
+ const runSessionShutdown = async (
754
+ ctx: AgentHookSessionShutdownContext,
755
+ ): Promise<void> => {
756
+ if (isAbortReason(ctx.reason)) {
757
+ const abortCommands = commandMap.agent_abort ?? [];
758
+ if (abortCommands.length > 0) {
759
+ runAsyncHookCommands({
760
+ commands: abortCommands,
761
+ cwd: options.cwd,
762
+ logger: options.logger,
763
+ payload: {
764
+ ...createPayloadBase(ctx, options),
765
+ hookName: "agent_abort",
766
+ reason: ctx.reason,
767
+ taskCancel: { taskMetadata: {} },
768
+ },
769
+ });
770
+ }
771
+ }
772
+ const shutdownCommands = commandMap.session_shutdown ?? [];
773
+ if (shutdownCommands.length === 0) {
774
+ return;
775
+ }
776
+ runAsyncHookCommands({
777
+ commands: shutdownCommands,
778
+ cwd: options.cwd,
779
+ logger: options.logger,
780
+ payload: {
781
+ ...createPayloadBase(ctx, options),
782
+ hookName: "session_shutdown",
783
+ reason: ctx.reason,
784
+ },
785
+ });
786
+ };
787
+
788
+ return {
789
+ onRunStart: async (ctx: AgentHookRunStartContext) => {
790
+ await runStartPayload(ctx);
791
+ return undefined;
792
+ },
793
+ onToolCallStart: async (ctx: AgentHookToolCallStartContext) =>
794
+ runToolCallStart(ctx),
795
+ onToolCallEnd: async (ctx: AgentHookToolCallEndContext) => {
796
+ await runToolCallEnd(ctx);
797
+ return undefined;
798
+ },
799
+ onTurnEnd: async (ctx: AgentHookTurnEndContext) => {
800
+ await runTurnEnd(ctx);
801
+ return undefined;
802
+ },
803
+ onStopError: async (ctx: AgentHookStopErrorContext) => {
804
+ await runStopError(ctx);
805
+ return undefined;
806
+ },
807
+ onSessionShutdown: async (ctx: AgentHookSessionShutdownContext) => {
808
+ await runSessionShutdown(ctx);
809
+ return undefined;
810
+ },
811
+ };
812
+ }
813
+
814
+ function mergeHookFunction<K extends keyof AgentHooks>(
815
+ layers: AgentHooks[],
816
+ key: K,
817
+ ): AgentHooks[K] | undefined {
818
+ const handlers = layers
819
+ .map((layer) => layer[key])
820
+ .filter((handler) => typeof handler === "function");
821
+ if (handlers.length === 0) {
822
+ return undefined;
823
+ }
824
+ return (async (ctx: unknown) => {
825
+ let control: AgentHookControl | undefined;
826
+ for (const handler of handlers) {
827
+ const next = await (handler as (arg: unknown) => unknown)(ctx);
828
+ control = mergeHookControls(
829
+ control,
830
+ next as AgentHookControl | undefined,
831
+ );
832
+ }
833
+ return control;
834
+ }) as AgentHooks[K];
835
+ }
836
+
837
+ export function mergeAgentHooks(
838
+ layers: Array<AgentHooks | undefined>,
839
+ ): AgentHooks | undefined {
840
+ const activeLayers = layers.filter(
841
+ (layer): layer is AgentHooks => layer !== undefined,
842
+ );
843
+ if (activeLayers.length === 0) {
844
+ return undefined;
845
+ }
846
+
847
+ return {
848
+ onRunStart: mergeHookFunction(activeLayers, "onRunStart"),
849
+ onRunEnd: mergeHookFunction(activeLayers, "onRunEnd"),
850
+ onIterationStart: mergeHookFunction(activeLayers, "onIterationStart"),
851
+ onIterationEnd: mergeHookFunction(activeLayers, "onIterationEnd"),
852
+ onTurnStart: mergeHookFunction(activeLayers, "onTurnStart"),
853
+ onTurnEnd: mergeHookFunction(activeLayers, "onTurnEnd"),
854
+ onToolCallStart: mergeHookFunction(activeLayers, "onToolCallStart"),
855
+ onToolCallEnd: mergeHookFunction(activeLayers, "onToolCallEnd"),
856
+ onSessionShutdown: mergeHookFunction(activeLayers, "onSessionShutdown"),
857
+ onError: mergeHookFunction(activeLayers, "onError"),
858
+ };
859
+ }