@clinebot/core 0.0.0

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