@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,262 @@
1
+ import type {
2
+ McpToolCallRequest,
3
+ McpToolCallResult,
4
+ McpToolDescriptor,
5
+ } from "@clinebot/agents";
6
+ import type {
7
+ McpConnectionStatus,
8
+ McpManager,
9
+ McpManagerOptions,
10
+ McpServerClient,
11
+ McpServerRegistration,
12
+ McpServerSnapshot,
13
+ } from "./types";
14
+
15
+ const DEFAULT_TOOLS_CACHE_TTL_MS = 5000;
16
+
17
+ type ManagedServerState = {
18
+ registration: McpServerRegistration;
19
+ client?: McpServerClient;
20
+ status: McpConnectionStatus;
21
+ lastError?: string;
22
+ updatedAt: number;
23
+ toolCache?: readonly McpToolDescriptor[];
24
+ toolCacheUpdatedAt?: number;
25
+ };
26
+
27
+ function nowMs(): number {
28
+ return Date.now();
29
+ }
30
+
31
+ function cloneTools(
32
+ tools: readonly McpToolDescriptor[],
33
+ ): readonly McpToolDescriptor[] {
34
+ return tools.map((tool) => ({
35
+ name: tool.name,
36
+ description: tool.description,
37
+ inputSchema: tool.inputSchema,
38
+ }));
39
+ }
40
+
41
+ export class InMemoryMcpManager implements McpManager {
42
+ private readonly toolsCacheTtlMs: number;
43
+ private readonly clientFactory: McpManagerOptions["clientFactory"];
44
+ private readonly servers = new Map<string, ManagedServerState>();
45
+ private readonly operationLocks = new Map<string, Promise<void>>();
46
+
47
+ constructor(options: McpManagerOptions) {
48
+ this.clientFactory = options.clientFactory;
49
+ this.toolsCacheTtlMs =
50
+ options.toolsCacheTtlMs ?? DEFAULT_TOOLS_CACHE_TTL_MS;
51
+ }
52
+
53
+ async registerServer(registration: McpServerRegistration): Promise<void> {
54
+ await this.runExclusive(registration.name, async () => {
55
+ const existing = this.servers.get(registration.name);
56
+ if (!existing) {
57
+ this.servers.set(registration.name, {
58
+ registration: { ...registration },
59
+ status: "disconnected",
60
+ updatedAt: nowMs(),
61
+ });
62
+ return;
63
+ }
64
+
65
+ const didTransportChange =
66
+ JSON.stringify(existing.registration.transport) !==
67
+ JSON.stringify(registration.transport);
68
+ existing.registration = { ...registration };
69
+ existing.updatedAt = nowMs();
70
+
71
+ if (didTransportChange) {
72
+ await this.disconnectState(existing);
73
+ existing.client = undefined;
74
+ existing.toolCache = undefined;
75
+ existing.toolCacheUpdatedAt = undefined;
76
+ }
77
+ });
78
+ }
79
+
80
+ async unregisterServer(serverName: string): Promise<void> {
81
+ await this.runExclusive(serverName, async () => {
82
+ const state = this.requireServer(serverName);
83
+ await this.disconnectState(state);
84
+ this.servers.delete(serverName);
85
+ });
86
+ }
87
+
88
+ async connectServer(serverName: string): Promise<void> {
89
+ await this.runExclusive(serverName, async () => {
90
+ const state = this.requireServer(serverName);
91
+ await this.connectState(state);
92
+ });
93
+ }
94
+
95
+ async disconnectServer(serverName: string): Promise<void> {
96
+ await this.runExclusive(serverName, async () => {
97
+ const state = this.requireServer(serverName);
98
+ await this.disconnectState(state);
99
+ });
100
+ }
101
+
102
+ async setServerDisabled(
103
+ serverName: string,
104
+ disabled: boolean,
105
+ ): Promise<void> {
106
+ await this.runExclusive(serverName, async () => {
107
+ const state = this.requireServer(serverName);
108
+ state.registration = {
109
+ ...state.registration,
110
+ disabled,
111
+ };
112
+ state.updatedAt = nowMs();
113
+ if (disabled) {
114
+ await this.disconnectState(state);
115
+ }
116
+ });
117
+ }
118
+
119
+ listServers(): readonly McpServerSnapshot[] {
120
+ return [...this.servers.values()]
121
+ .map((state) => ({
122
+ name: state.registration.name,
123
+ status: state.status,
124
+ disabled: state.registration.disabled === true,
125
+ lastError: state.lastError,
126
+ toolCount: state.toolCache?.length ?? 0,
127
+ updatedAt: state.updatedAt,
128
+ metadata: state.registration.metadata,
129
+ }))
130
+ .sort((a, b) => a.name.localeCompare(b.name));
131
+ }
132
+
133
+ async listTools(serverName: string): Promise<readonly McpToolDescriptor[]> {
134
+ const state = this.requireServer(serverName);
135
+ const fetchedAt = state.toolCacheUpdatedAt ?? 0;
136
+ if (state.toolCache && nowMs() - fetchedAt <= this.toolsCacheTtlMs) {
137
+ return state.toolCache;
138
+ }
139
+ return this.refreshTools(serverName);
140
+ }
141
+
142
+ async refreshTools(
143
+ serverName: string,
144
+ ): Promise<readonly McpToolDescriptor[]> {
145
+ return this.runExclusive(serverName, async () => {
146
+ const state = this.requireServer(serverName);
147
+ const client = await this.ensureConnectedClient(state);
148
+ const tools = await client.listTools();
149
+ const cloned = cloneTools(tools);
150
+ state.toolCache = cloned;
151
+ state.toolCacheUpdatedAt = nowMs();
152
+ state.updatedAt = nowMs();
153
+ return cloned;
154
+ });
155
+ }
156
+
157
+ async callTool(request: McpToolCallRequest): Promise<McpToolCallResult> {
158
+ return this.runExclusive(request.serverName, async () => {
159
+ const state = this.requireServer(request.serverName);
160
+ const client = await this.ensureConnectedClient(state);
161
+ state.updatedAt = nowMs();
162
+ return client.callTool({
163
+ name: request.toolName,
164
+ arguments: request.arguments,
165
+ context: request.context,
166
+ });
167
+ });
168
+ }
169
+
170
+ async dispose(): Promise<void> {
171
+ const names = [...this.servers.keys()];
172
+ for (const name of names) {
173
+ await this.unregisterServer(name);
174
+ }
175
+ }
176
+
177
+ private async ensureConnectedClient(
178
+ state: ManagedServerState,
179
+ ): Promise<McpServerClient> {
180
+ await this.connectState(state);
181
+ if (!state.client) {
182
+ throw new Error(
183
+ `MCP server "${state.registration.name}" does not have an initialized client.`,
184
+ );
185
+ }
186
+ return state.client;
187
+ }
188
+
189
+ private async connectState(state: ManagedServerState): Promise<void> {
190
+ if (state.registration.disabled) {
191
+ throw new Error(
192
+ `MCP server "${state.registration.name}" is disabled and cannot be connected.`,
193
+ );
194
+ }
195
+ if (state.status === "connected" && state.client) {
196
+ return;
197
+ }
198
+ state.status = "connecting";
199
+ state.updatedAt = nowMs();
200
+ try {
201
+ const client =
202
+ state.client ?? (await this.clientFactory(state.registration));
203
+ await client.connect();
204
+ state.client = client;
205
+ state.status = "connected";
206
+ state.lastError = undefined;
207
+ state.updatedAt = nowMs();
208
+ } catch (error) {
209
+ state.status = "disconnected";
210
+ state.lastError = error instanceof Error ? error.message : String(error);
211
+ state.updatedAt = nowMs();
212
+ throw error;
213
+ }
214
+ }
215
+
216
+ private async disconnectState(state: ManagedServerState): Promise<void> {
217
+ if (!state.client) {
218
+ state.status = "disconnected";
219
+ state.updatedAt = nowMs();
220
+ return;
221
+ }
222
+
223
+ try {
224
+ await state.client.disconnect();
225
+ } finally {
226
+ state.status = "disconnected";
227
+ state.updatedAt = nowMs();
228
+ }
229
+ }
230
+
231
+ private requireServer(serverName: string): ManagedServerState {
232
+ const state = this.servers.get(serverName);
233
+ if (!state) {
234
+ throw new Error(`Unknown MCP server: ${serverName}`);
235
+ }
236
+ return state;
237
+ }
238
+
239
+ private async runExclusive<T>(
240
+ serverName: string,
241
+ operation: () => Promise<T>,
242
+ ): Promise<T> {
243
+ const previous = this.operationLocks.get(serverName) ?? Promise.resolve();
244
+ let releaseCurrent: (() => void) | undefined;
245
+ const current = new Promise<void>((resolve) => {
246
+ releaseCurrent = resolve;
247
+ });
248
+ const queued = previous.catch(() => undefined).then(() => current);
249
+ this.operationLocks.set(serverName, queued);
250
+
251
+ await previous.catch(() => undefined);
252
+ try {
253
+ return await operation();
254
+ } finally {
255
+ releaseCurrent?.();
256
+ const lock = this.operationLocks.get(serverName);
257
+ if (lock === queued) {
258
+ this.operationLocks.delete(serverName);
259
+ }
260
+ }
261
+ }
262
+ }
@@ -0,0 +1,88 @@
1
+ import type {
2
+ McpToolCallRequest,
3
+ McpToolCallResult,
4
+ McpToolDescriptor,
5
+ McpToolProvider,
6
+ ToolContext,
7
+ } from "@clinebot/agents";
8
+
9
+ export type McpConnectionStatus = "disconnected" | "connecting" | "connected";
10
+
11
+ export interface McpStdioTransportConfig {
12
+ type: "stdio";
13
+ command: string;
14
+ args?: string[];
15
+ cwd?: string;
16
+ env?: Record<string, string>;
17
+ }
18
+
19
+ export interface McpSseTransportConfig {
20
+ type: "sse";
21
+ url: string;
22
+ headers?: Record<string, string>;
23
+ }
24
+
25
+ export interface McpStreamableHttpTransportConfig {
26
+ type: "streamableHttp";
27
+ url: string;
28
+ headers?: Record<string, string>;
29
+ }
30
+
31
+ export type McpServerTransportConfig =
32
+ | McpStdioTransportConfig
33
+ | McpSseTransportConfig
34
+ | McpStreamableHttpTransportConfig;
35
+
36
+ export interface McpServerRegistration {
37
+ name: string;
38
+ transport: McpServerTransportConfig;
39
+ disabled?: boolean;
40
+ metadata?: Record<string, unknown>;
41
+ }
42
+
43
+ export interface McpServerSnapshot {
44
+ name: string;
45
+ status: McpConnectionStatus;
46
+ disabled: boolean;
47
+ lastError?: string;
48
+ toolCount: number;
49
+ updatedAt: number;
50
+ metadata?: Record<string, unknown>;
51
+ }
52
+
53
+ export interface McpServerClient {
54
+ connect(): Promise<void>;
55
+ disconnect(): Promise<void>;
56
+ listTools(): Promise<readonly McpToolDescriptor[]>;
57
+ callTool(request: {
58
+ name: string;
59
+ arguments?: Record<string, unknown>;
60
+ context?: ToolContext;
61
+ }): Promise<McpToolCallResult>;
62
+ }
63
+
64
+ export type McpServerClientFactory = (
65
+ registration: McpServerRegistration,
66
+ ) => Promise<McpServerClient> | McpServerClient;
67
+
68
+ export interface McpManagerOptions {
69
+ clientFactory: McpServerClientFactory;
70
+ /**
71
+ * Cache TTL for tools/list responses.
72
+ * A short cache avoids repeated list requests while keeping server metadata fresh.
73
+ * @default 5000
74
+ */
75
+ toolsCacheTtlMs?: number;
76
+ }
77
+
78
+ export interface McpManager extends McpToolProvider {
79
+ registerServer(registration: McpServerRegistration): Promise<void>;
80
+ unregisterServer(serverName: string): Promise<void>;
81
+ connectServer(serverName: string): Promise<void>;
82
+ disconnectServer(serverName: string): Promise<void>;
83
+ setServerDisabled(serverName: string, disabled: boolean): Promise<void>;
84
+ listServers(): readonly McpServerSnapshot[];
85
+ refreshTools(serverName: string): Promise<readonly McpToolDescriptor[]>;
86
+ callTool(request: McpToolCallRequest): Promise<McpToolCallResult>;
87
+ dispose(): Promise<void>;
88
+ }
@@ -0,0 +1,106 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { createHookConfigFileHooks } from "./hook-file-hooks";
6
+
7
+ async function createWorkspaceWithHook(
8
+ fileName: string,
9
+ body: string,
10
+ ): Promise<{ workspace: string; hookPath: string }> {
11
+ const workspace = await mkdtemp(join(tmpdir(), "hooks-workspace-"));
12
+ const hooksDir = join(workspace, ".clinerules", "hooks");
13
+ await mkdir(hooksDir, { recursive: true });
14
+ const hookPath = join(hooksDir, fileName);
15
+ await writeFile(hookPath, body, "utf8");
16
+ return { workspace, hookPath };
17
+ }
18
+
19
+ describe("createHookConfigFileHooks", () => {
20
+ it("executes extensionless legacy hook files via bash fallback", async () => {
21
+ const { workspace } = await createWorkspaceWithHook(
22
+ "PreToolUse",
23
+ 'echo \'HOOK_CONTROL\t{"cancel":true,"context":"legacy-ok"}\'\n',
24
+ );
25
+ try {
26
+ const hooks = createHookConfigFileHooks({
27
+ cwd: workspace,
28
+ workspacePath: workspace,
29
+ });
30
+ expect(hooks?.onToolCallStart).toBeTypeOf("function");
31
+ const control = await hooks?.onToolCallStart?.({
32
+ agentId: "agent_1",
33
+ conversationId: "conv_1",
34
+ parentAgentId: null,
35
+ iteration: 1,
36
+ call: {
37
+ id: "call_1",
38
+ name: "read_file",
39
+ input: { path: "README.md" },
40
+ },
41
+ });
42
+ expect(control).toMatchObject({ cancel: true, context: "legacy-ok" });
43
+ } finally {
44
+ await rm(workspace, { recursive: true, force: true });
45
+ }
46
+ });
47
+
48
+ it("honors shebang interpreter when present", async () => {
49
+ const { workspace } = await createWorkspaceWithHook(
50
+ "PreToolUse",
51
+ '#!/usr/bin/env bash\necho \'HOOK_CONTROL\t{"cancel":false,"context":"shebang-ok"}\'\n',
52
+ );
53
+ try {
54
+ const hooks = createHookConfigFileHooks({
55
+ cwd: workspace,
56
+ workspacePath: workspace,
57
+ });
58
+ expect(hooks?.onToolCallStart).toBeTypeOf("function");
59
+ const control = await hooks?.onToolCallStart?.({
60
+ agentId: "agent_1",
61
+ conversationId: "conv_1",
62
+ parentAgentId: null,
63
+ iteration: 1,
64
+ call: {
65
+ id: "call_1",
66
+ name: "read_file",
67
+ input: { path: "README.md" },
68
+ },
69
+ });
70
+ expect(control).toMatchObject({ cancel: false, context: "shebang-ok" });
71
+ } finally {
72
+ await rm(workspace, { recursive: true, force: true });
73
+ }
74
+ });
75
+
76
+ it("parses review control from hook output", async () => {
77
+ const { workspace } = await createWorkspaceWithHook(
78
+ "PreToolUse.ts",
79
+ 'console.log(\'HOOK_CONTROL\\t{"review":true,"context":"needs-review"}\')\n',
80
+ );
81
+ try {
82
+ const hooks = createHookConfigFileHooks({
83
+ cwd: workspace,
84
+ workspacePath: workspace,
85
+ });
86
+ expect(hooks?.onToolCallStart).toBeTypeOf("function");
87
+ const control = await hooks?.onToolCallStart?.({
88
+ agentId: "agent_1",
89
+ conversationId: "conv_1",
90
+ parentAgentId: null,
91
+ iteration: 1,
92
+ call: {
93
+ id: "call_1",
94
+ name: "run_commands",
95
+ input: { commands: ["git status"] },
96
+ },
97
+ });
98
+ expect(control).toMatchObject({
99
+ review: true,
100
+ context: "needs-review",
101
+ });
102
+ } finally {
103
+ await rm(workspace, { recursive: true, force: true });
104
+ }
105
+ });
106
+ });