@bluehawks/cli 1.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 (176) hide show
  1. package/.eslintrc.json +36 -0
  2. package/.prettierrc +8 -0
  3. package/README.md +288 -0
  4. package/dist/cli/app.d.ts +12 -0
  5. package/dist/cli/app.d.ts.map +1 -0
  6. package/dist/cli/app.js +201 -0
  7. package/dist/cli/app.js.map +1 -0
  8. package/dist/cli/commands/index.d.ts +56 -0
  9. package/dist/cli/commands/index.d.ts.map +1 -0
  10. package/dist/cli/commands/index.js +201 -0
  11. package/dist/cli/commands/index.js.map +1 -0
  12. package/dist/config/constants.d.ts +32 -0
  13. package/dist/config/constants.d.ts.map +1 -0
  14. package/dist/config/constants.js +39 -0
  15. package/dist/config/constants.js.map +1 -0
  16. package/dist/config/index.d.ts +4 -0
  17. package/dist/config/index.d.ts.map +1 -0
  18. package/dist/config/index.js +4 -0
  19. package/dist/config/index.js.map +1 -0
  20. package/dist/config/schema.d.ts +56 -0
  21. package/dist/config/schema.d.ts.map +1 -0
  22. package/dist/config/schema.js +28 -0
  23. package/dist/config/schema.js.map +1 -0
  24. package/dist/config/settings.d.ts +20 -0
  25. package/dist/config/settings.d.ts.map +1 -0
  26. package/dist/config/settings.js +102 -0
  27. package/dist/config/settings.js.map +1 -0
  28. package/dist/core/agents/agent.d.ts +33 -0
  29. package/dist/core/agents/agent.d.ts.map +1 -0
  30. package/dist/core/agents/agent.js +156 -0
  31. package/dist/core/agents/agent.js.map +1 -0
  32. package/dist/core/agents/index.d.ts +3 -0
  33. package/dist/core/agents/index.d.ts.map +1 -0
  34. package/dist/core/agents/index.js +3 -0
  35. package/dist/core/agents/index.js.map +1 -0
  36. package/dist/core/agents/orchestrator.d.ts +56 -0
  37. package/dist/core/agents/orchestrator.d.ts.map +1 -0
  38. package/dist/core/agents/orchestrator.js +151 -0
  39. package/dist/core/agents/orchestrator.js.map +1 -0
  40. package/dist/core/api/client.d.ts +46 -0
  41. package/dist/core/api/client.d.ts.map +1 -0
  42. package/dist/core/api/client.js +223 -0
  43. package/dist/core/api/client.js.map +1 -0
  44. package/dist/core/api/index.d.ts +3 -0
  45. package/dist/core/api/index.d.ts.map +1 -0
  46. package/dist/core/api/index.js +3 -0
  47. package/dist/core/api/index.js.map +1 -0
  48. package/dist/core/api/types.d.ts +126 -0
  49. package/dist/core/api/types.d.ts.map +1 -0
  50. package/dist/core/api/types.js +16 -0
  51. package/dist/core/api/types.js.map +1 -0
  52. package/dist/core/hooks/index.d.ts +3 -0
  53. package/dist/core/hooks/index.d.ts.map +1 -0
  54. package/dist/core/hooks/index.js +3 -0
  55. package/dist/core/hooks/index.js.map +1 -0
  56. package/dist/core/hooks/manager.d.ts +43 -0
  57. package/dist/core/hooks/manager.d.ts.map +1 -0
  58. package/dist/core/hooks/manager.js +178 -0
  59. package/dist/core/hooks/manager.js.map +1 -0
  60. package/dist/core/hooks/types.d.ts +68 -0
  61. package/dist/core/hooks/types.d.ts.map +1 -0
  62. package/dist/core/hooks/types.js +6 -0
  63. package/dist/core/hooks/types.js.map +1 -0
  64. package/dist/core/mcp/client.d.ts +48 -0
  65. package/dist/core/mcp/client.d.ts.map +1 -0
  66. package/dist/core/mcp/client.js +139 -0
  67. package/dist/core/mcp/client.js.map +1 -0
  68. package/dist/core/mcp/index.d.ts +3 -0
  69. package/dist/core/mcp/index.d.ts.map +1 -0
  70. package/dist/core/mcp/index.js +3 -0
  71. package/dist/core/mcp/index.js.map +1 -0
  72. package/dist/core/mcp/manager.d.ts +46 -0
  73. package/dist/core/mcp/manager.d.ts.map +1 -0
  74. package/dist/core/mcp/manager.js +133 -0
  75. package/dist/core/mcp/manager.js.map +1 -0
  76. package/dist/core/plugins/index.d.ts +3 -0
  77. package/dist/core/plugins/index.d.ts.map +1 -0
  78. package/dist/core/plugins/index.js +3 -0
  79. package/dist/core/plugins/index.js.map +1 -0
  80. package/dist/core/plugins/loader.d.ts +63 -0
  81. package/dist/core/plugins/loader.d.ts.map +1 -0
  82. package/dist/core/plugins/loader.js +258 -0
  83. package/dist/core/plugins/loader.js.map +1 -0
  84. package/dist/core/plugins/types.d.ts +95 -0
  85. package/dist/core/plugins/types.d.ts.map +1 -0
  86. package/dist/core/plugins/types.js +6 -0
  87. package/dist/core/plugins/types.js.map +1 -0
  88. package/dist/core/session/index.d.ts +3 -0
  89. package/dist/core/session/index.d.ts.map +1 -0
  90. package/dist/core/session/index.js +3 -0
  91. package/dist/core/session/index.js.map +1 -0
  92. package/dist/core/session/manager.d.ts +57 -0
  93. package/dist/core/session/manager.d.ts.map +1 -0
  94. package/dist/core/session/manager.js +182 -0
  95. package/dist/core/session/manager.js.map +1 -0
  96. package/dist/core/session/storage.d.ts +42 -0
  97. package/dist/core/session/storage.d.ts.map +1 -0
  98. package/dist/core/session/storage.js +138 -0
  99. package/dist/core/session/storage.js.map +1 -0
  100. package/dist/core/tools/definitions/file.d.ts +6 -0
  101. package/dist/core/tools/definitions/file.d.ts.map +1 -0
  102. package/dist/core/tools/definitions/file.js +276 -0
  103. package/dist/core/tools/definitions/file.js.map +1 -0
  104. package/dist/core/tools/definitions/git.d.ts +6 -0
  105. package/dist/core/tools/definitions/git.d.ts.map +1 -0
  106. package/dist/core/tools/definitions/git.js +294 -0
  107. package/dist/core/tools/definitions/git.js.map +1 -0
  108. package/dist/core/tools/definitions/index.d.ts +11 -0
  109. package/dist/core/tools/definitions/index.d.ts.map +1 -0
  110. package/dist/core/tools/definitions/index.js +22 -0
  111. package/dist/core/tools/definitions/index.js.map +1 -0
  112. package/dist/core/tools/definitions/search.d.ts +6 -0
  113. package/dist/core/tools/definitions/search.d.ts.map +1 -0
  114. package/dist/core/tools/definitions/search.js +223 -0
  115. package/dist/core/tools/definitions/search.js.map +1 -0
  116. package/dist/core/tools/definitions/shell.d.ts +6 -0
  117. package/dist/core/tools/definitions/shell.d.ts.map +1 -0
  118. package/dist/core/tools/definitions/shell.js +190 -0
  119. package/dist/core/tools/definitions/shell.js.map +1 -0
  120. package/dist/core/tools/definitions/web.d.ts +6 -0
  121. package/dist/core/tools/definitions/web.d.ts.map +1 -0
  122. package/dist/core/tools/definitions/web.js +104 -0
  123. package/dist/core/tools/definitions/web.js.map +1 -0
  124. package/dist/core/tools/executor.d.ts +24 -0
  125. package/dist/core/tools/executor.d.ts.map +1 -0
  126. package/dist/core/tools/executor.js +111 -0
  127. package/dist/core/tools/executor.js.map +1 -0
  128. package/dist/core/tools/index.d.ts +4 -0
  129. package/dist/core/tools/index.d.ts.map +1 -0
  130. package/dist/core/tools/index.js +4 -0
  131. package/dist/core/tools/index.js.map +1 -0
  132. package/dist/core/tools/registry.d.ts +23 -0
  133. package/dist/core/tools/registry.d.ts.map +1 -0
  134. package/dist/core/tools/registry.js +28 -0
  135. package/dist/core/tools/registry.js.map +1 -0
  136. package/dist/index.d.ts +7 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.js +352 -0
  139. package/dist/index.js.map +1 -0
  140. package/package.json +62 -0
  141. package/src/cli/app.tsx +319 -0
  142. package/src/cli/commands/index.ts +261 -0
  143. package/src/config/constants.ts +45 -0
  144. package/src/config/index.ts +3 -0
  145. package/src/config/schema.ts +36 -0
  146. package/src/config/settings.ts +121 -0
  147. package/src/core/agents/agent.ts +205 -0
  148. package/src/core/agents/index.ts +2 -0
  149. package/src/core/agents/orchestrator.ts +223 -0
  150. package/src/core/api/client.ts +300 -0
  151. package/src/core/api/index.ts +2 -0
  152. package/src/core/api/types.ts +149 -0
  153. package/src/core/hooks/index.ts +2 -0
  154. package/src/core/hooks/manager.ts +212 -0
  155. package/src/core/hooks/types.ts +116 -0
  156. package/src/core/mcp/client.ts +198 -0
  157. package/src/core/mcp/index.ts +2 -0
  158. package/src/core/mcp/manager.ts +153 -0
  159. package/src/core/plugins/index.ts +2 -0
  160. package/src/core/plugins/loader.ts +312 -0
  161. package/src/core/plugins/types.ts +111 -0
  162. package/src/core/session/index.ts +2 -0
  163. package/src/core/session/manager.ts +246 -0
  164. package/src/core/session/storage.ts +184 -0
  165. package/src/core/tools/definitions/file.ts +312 -0
  166. package/src/core/tools/definitions/git.ts +326 -0
  167. package/src/core/tools/definitions/index.ts +24 -0
  168. package/src/core/tools/definitions/search.ts +266 -0
  169. package/src/core/tools/definitions/shell.ts +228 -0
  170. package/src/core/tools/definitions/web.ts +113 -0
  171. package/src/core/tools/executor.ts +145 -0
  172. package/src/core/tools/index.ts +3 -0
  173. package/src/core/tools/registry.ts +44 -0
  174. package/src/index.ts +407 -0
  175. package/tsconfig.json +40 -0
  176. package/vitest.config.ts +13 -0
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Bluehawks CLI - API Client
3
+ * OpenAI-compatible API client with streaming support
4
+ */
5
+
6
+ import {
7
+ API_BASE_URL,
8
+ DEFAULT_MODEL,
9
+ DEFAULT_MAX_TOKENS,
10
+ DEFAULT_TEMPERATURE,
11
+ DEFAULT_TIMEOUT_MS,
12
+ MAX_RETRIES,
13
+ RETRY_DELAY_MS,
14
+ } from '../../config/constants.js';
15
+ import {
16
+ type APIClientOptions,
17
+ type ChatCompletionRequest,
18
+ type ChatCompletionResponse,
19
+ type ChatCompletionChunk,
20
+ type Message,
21
+ type ToolCall,
22
+ type ToolDefinition,
23
+ type ToolResult,
24
+ type StreamDelta,
25
+ APIError,
26
+ } from './types.js';
27
+
28
+ export class APIClient {
29
+ private baseUrl: string;
30
+ private apiKey: string;
31
+ private model: string;
32
+ private maxTokens: number;
33
+ private temperature: number;
34
+ private timeout: number;
35
+
36
+ constructor(options: APIClientOptions = {}) {
37
+ this.baseUrl = options.baseUrl || process.env.BLUEHAWKS_API_URL || API_BASE_URL;
38
+ this.apiKey = options.apiKey || process.env.BLUEHAWKS_API_KEY || '';
39
+ this.model = options.model || process.env.BLUEHAWKS_MODEL || DEFAULT_MODEL;
40
+ this.maxTokens = options.maxTokens || DEFAULT_MAX_TOKENS;
41
+ this.temperature = options.temperature || DEFAULT_TEMPERATURE;
42
+ this.timeout = options.timeout || DEFAULT_TIMEOUT_MS;
43
+ }
44
+
45
+ /**
46
+ * Create a chat completion (non-streaming)
47
+ */
48
+ async createChatCompletion(
49
+ messages: Message[],
50
+ tools?: ToolDefinition[],
51
+ toolChoice?: ChatCompletionRequest['tool_choice']
52
+ ): Promise<ChatCompletionResponse> {
53
+ const request: ChatCompletionRequest = {
54
+ model: this.model,
55
+ messages,
56
+ max_tokens: this.maxTokens,
57
+ temperature: this.temperature,
58
+ stream: false,
59
+ };
60
+
61
+ if (tools && tools.length > 0) {
62
+ request.tools = tools;
63
+ request.tool_choice = toolChoice || 'auto';
64
+ }
65
+
66
+ const response = await this.makeRequest<ChatCompletionResponse>('/chat/completions', request);
67
+
68
+ return response;
69
+ }
70
+
71
+ /**
72
+ * Create a streaming chat completion
73
+ */
74
+ async *createStreamingChatCompletion(
75
+ messages: Message[],
76
+ _tools?: ToolDefinition[],
77
+ _toolChoice?: ChatCompletionRequest['tool_choice']
78
+ ): AsyncGenerator<ChatCompletionChunk> {
79
+ const request: ChatCompletionRequest = {
80
+ model: this.model,
81
+ messages,
82
+ max_tokens: this.maxTokens,
83
+ temperature: this.temperature,
84
+ stream: true,
85
+ };
86
+
87
+ // Note: Many vLLM backends don't support tool_choice in streaming mode
88
+ // So we skip sending tools for streaming requests.
89
+ // For tool-based workflows, use non-streaming createChatCompletion instead.
90
+
91
+ const response = await this.fetchWithRetry('/chat/completions', {
92
+ method: 'POST',
93
+ headers: this.getHeaders(),
94
+ body: JSON.stringify(request),
95
+ });
96
+
97
+ if (!response.ok) {
98
+ const errorBody = await response.text();
99
+ throw new APIError(`API request failed: ${response.status}`, response.status, errorBody);
100
+ }
101
+
102
+ if (!response.body) {
103
+ throw new APIError('Response body is empty');
104
+ }
105
+
106
+ const reader = response.body.getReader();
107
+ const decoder = new TextDecoder();
108
+ let buffer = '';
109
+
110
+ try {
111
+ while (true) {
112
+ const { done, value } = await reader.read();
113
+ if (done) break;
114
+
115
+ buffer += decoder.decode(value, { stream: true });
116
+ const lines = buffer.split('\n');
117
+ buffer = lines.pop() || '';
118
+
119
+ for (const line of lines) {
120
+ const trimmed = line.trim();
121
+ if (!trimmed || trimmed === 'data: [DONE]') continue;
122
+ if (!trimmed.startsWith('data: ')) continue;
123
+
124
+ try {
125
+ const data = trimmed.slice(6);
126
+ const chunk = JSON.parse(data) as ChatCompletionChunk;
127
+ yield chunk;
128
+ } catch {
129
+ // Skip invalid JSON
130
+ }
131
+ }
132
+ }
133
+ } finally {
134
+ reader.releaseLock();
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Convenience method to stream and collect tool calls
140
+ */
141
+ async streamChatCompletion(
142
+ messages: Message[],
143
+ tools?: ToolDefinition[],
144
+ onChunk?: (content: string) => void,
145
+ onToolCalls?: (toolCalls: ToolCall[]) => void
146
+ ): Promise<{ content: string; toolCalls: ToolCall[]; finishReason: string }> {
147
+ let content = '';
148
+ const toolCalls: ToolCall[] = [];
149
+ const toolCallsMap = new Map<number, ToolCall>();
150
+ let finishReason = 'stop';
151
+
152
+ for await (const chunk of this.createStreamingChatCompletion(messages, tools)) {
153
+ for (const choice of chunk.choices) {
154
+ if (choice.finish_reason) {
155
+ finishReason = choice.finish_reason;
156
+ }
157
+
158
+ const deltaContent = choice.delta.content;
159
+ if (typeof deltaContent === 'string') {
160
+ content += deltaContent;
161
+ onChunk?.(deltaContent);
162
+ }
163
+
164
+ const deltaToolCalls = choice.delta.tool_calls as StreamDelta['tool_calls'];
165
+ if (deltaToolCalls) {
166
+ for (const tc of deltaToolCalls) {
167
+ const index = tc.index ?? 0;
168
+ const existing = toolCallsMap.get(index);
169
+ if (existing) {
170
+ // Append to existing tool call
171
+ if (tc.function?.arguments) {
172
+ existing.function.arguments += tc.function.arguments;
173
+ }
174
+ } else if (tc.id) {
175
+ // New tool call
176
+ const newToolCall: ToolCall = {
177
+ id: tc.id,
178
+ type: 'function',
179
+ function: {
180
+ name: tc.function?.name || '',
181
+ arguments: tc.function?.arguments || '',
182
+ },
183
+ };
184
+ toolCallsMap.set(index, newToolCall);
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ // Convert map to array
192
+ toolCalls.push(...toolCallsMap.values());
193
+
194
+ if (toolCalls.length > 0) {
195
+ onToolCalls?.(toolCalls);
196
+ }
197
+
198
+ return { content, toolCalls, finishReason };
199
+ }
200
+
201
+ /**
202
+ * Send tool results back to the API
203
+ */
204
+ async sendToolResults(
205
+ messages: Message[],
206
+ toolResults: ToolResult[],
207
+ tools?: ToolDefinition[],
208
+ onChunk?: (content: string) => void,
209
+ onToolCalls?: (toolCalls: ToolCall[]) => void
210
+ ): Promise<{ content: string; toolCalls: ToolCall[]; finishReason: string }> {
211
+ // Add tool results as messages
212
+ const toolMessages: Message[] = toolResults.map((result) => ({
213
+ role: 'tool' as const,
214
+ tool_call_id: result.tool_call_id,
215
+ content: result.content,
216
+ }));
217
+
218
+ const allMessages = [...messages, ...toolMessages];
219
+ return this.streamChatCompletion(allMessages, tools, onChunk, onToolCalls);
220
+ }
221
+
222
+ private getHeaders(): Record<string, string> {
223
+ const headers: Record<string, string> = {
224
+ 'Content-Type': 'application/json',
225
+ };
226
+
227
+ if (this.apiKey) {
228
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
229
+ }
230
+
231
+ return headers;
232
+ }
233
+
234
+ private async makeRequest<T>(endpoint: string, body: unknown): Promise<T> {
235
+ const response = await this.fetchWithRetry(endpoint, {
236
+ method: 'POST',
237
+ headers: this.getHeaders(),
238
+ body: JSON.stringify(body),
239
+ });
240
+
241
+ if (!response.ok) {
242
+ const errorBody = await response.text();
243
+ throw new APIError(`API request failed: ${response.status}`, response.status, errorBody);
244
+ }
245
+
246
+ return response.json() as Promise<T>;
247
+ }
248
+
249
+ private async fetchWithRetry(
250
+ endpoint: string,
251
+ options: RequestInit,
252
+ retries = MAX_RETRIES
253
+ ): Promise<Response> {
254
+ const url = `${this.baseUrl}${endpoint}`;
255
+ const controller = new AbortController();
256
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
257
+
258
+ try {
259
+ const response = await fetch(url, {
260
+ ...options,
261
+ signal: controller.signal,
262
+ });
263
+
264
+ clearTimeout(timeoutId);
265
+
266
+ // Retry on 5xx errors
267
+ if (response.status >= 500 && retries > 0) {
268
+ await this.delay(RETRY_DELAY_MS * (MAX_RETRIES - retries + 1));
269
+ return this.fetchWithRetry(endpoint, options, retries - 1);
270
+ }
271
+
272
+ return response;
273
+ } catch (error) {
274
+ clearTimeout(timeoutId);
275
+
276
+ if (retries > 0 && error instanceof Error && error.name !== 'AbortError') {
277
+ await this.delay(RETRY_DELAY_MS * (MAX_RETRIES - retries + 1));
278
+ return this.fetchWithRetry(endpoint, options, retries - 1);
279
+ }
280
+
281
+ throw new APIError(`Network error: ${error instanceof Error ? error.message : 'Unknown error'}`);
282
+ }
283
+ }
284
+
285
+ private delay(ms: number): Promise<void> {
286
+ return new Promise((resolve) => setTimeout(resolve, ms));
287
+ }
288
+
289
+ // Getters for current configuration
290
+ get currentModel(): string {
291
+ return this.model;
292
+ }
293
+
294
+ get currentBaseUrl(): string {
295
+ return this.baseUrl;
296
+ }
297
+ }
298
+
299
+ // Export singleton instance
300
+ export const apiClient = new APIClient();
@@ -0,0 +1,2 @@
1
+ export * from './client.js';
2
+ export * from './types.js';
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Bluehawks CLI - API Types
3
+ * OpenAI-compatible API type definitions
4
+ */
5
+
6
+ // Message Types
7
+ export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
8
+
9
+ export interface Message {
10
+ role: MessageRole;
11
+ content: string | ContentPart[];
12
+ name?: string;
13
+ tool_calls?: ToolCall[];
14
+ tool_call_id?: string;
15
+ }
16
+
17
+ export interface ContentPart {
18
+ type: 'text' | 'image_url';
19
+ text?: string;
20
+ image_url?: {
21
+ url: string;
22
+ detail?: 'auto' | 'low' | 'high';
23
+ };
24
+ }
25
+
26
+ // Tool Types
27
+ export interface ToolCall {
28
+ id: string;
29
+ type: 'function';
30
+ function: {
31
+ name: string;
32
+ arguments: string;
33
+ };
34
+ }
35
+
36
+ export interface ToolDefinition {
37
+ type: 'function';
38
+ function: {
39
+ name: string;
40
+ description: string;
41
+ parameters: {
42
+ type: 'object';
43
+ properties: Record<string, ToolParameterSchema>;
44
+ required?: string[];
45
+ };
46
+ };
47
+ }
48
+
49
+ export interface ToolParameterSchema {
50
+ type: string;
51
+ description?: string;
52
+ enum?: string[];
53
+ items?: ToolParameterSchema;
54
+ properties?: Record<string, ToolParameterSchema>;
55
+ required?: string[];
56
+ }
57
+
58
+ export interface ToolResult {
59
+ tool_call_id: string;
60
+ content: string;
61
+ isError?: boolean;
62
+ }
63
+
64
+ // API Request/Response Types
65
+ export interface ChatCompletionRequest {
66
+ model: string;
67
+ messages: Message[];
68
+ tools?: ToolDefinition[];
69
+ tool_choice?: 'auto' | 'none' | 'required' | { type: 'function'; function: { name: string } };
70
+ max_tokens?: number;
71
+ temperature?: number;
72
+ stream?: boolean;
73
+ stop?: string[];
74
+ }
75
+
76
+ export interface ChatCompletionResponse {
77
+ id: string;
78
+ object: 'chat.completion';
79
+ created: number;
80
+ model: string;
81
+ choices: Choice[];
82
+ usage?: Usage;
83
+ }
84
+
85
+ export interface Choice {
86
+ index: number;
87
+ message: Message;
88
+ finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | null;
89
+ }
90
+
91
+ export interface Usage {
92
+ prompt_tokens: number;
93
+ completion_tokens: number;
94
+ total_tokens: number;
95
+ }
96
+
97
+ // Streaming Types
98
+ export interface StreamDelta {
99
+ content?: string;
100
+ tool_calls?: Array<{
101
+ index?: number;
102
+ id?: string;
103
+ type?: 'function';
104
+ function?: {
105
+ name?: string;
106
+ arguments?: string;
107
+ };
108
+ }>;
109
+ }
110
+
111
+ export interface ChatCompletionChunk {
112
+ id: string;
113
+ object: 'chat.completion.chunk';
114
+ created: number;
115
+ model: string;
116
+ choices: StreamChoice[];
117
+ }
118
+
119
+ export interface StreamChoice {
120
+ index: number;
121
+ delta: StreamDelta;
122
+ finish_reason: string | null;
123
+ }
124
+
125
+ // API Client Options
126
+ export interface APIClientOptions {
127
+ baseUrl?: string;
128
+ apiKey?: string;
129
+ model?: string;
130
+ maxTokens?: number;
131
+ temperature?: number;
132
+ timeout?: number;
133
+ }
134
+
135
+ // Streaming callback types
136
+ export type StreamCallback = (chunk: string) => void;
137
+ export type ToolCallCallback = (toolCalls: ToolCall[]) => void;
138
+
139
+ // API Error
140
+ export class APIError extends Error {
141
+ constructor(
142
+ message: string,
143
+ public statusCode?: number,
144
+ public response?: unknown
145
+ ) {
146
+ super(message);
147
+ this.name = 'APIError';
148
+ }
149
+ }
@@ -0,0 +1,2 @@
1
+ export * from './types.js';
2
+ export * from './manager.js';
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Bluehawks CLI - Hooks Manager
3
+ * Central registry for hook handlers
4
+ */
5
+
6
+ import { spawn } from 'node:child_process';
7
+ import type {
8
+ HookEvent,
9
+ HookHandler,
10
+ HookInput,
11
+ HookOutput,
12
+ HooksConfig,
13
+ } from './types.js';
14
+
15
+ export class HooksManager {
16
+ private handlers: Map<HookEvent, HookHandler[]> = new Map();
17
+
18
+ constructor() {
19
+ // Initialize empty handler arrays for each event
20
+ const events: HookEvent[] = [
21
+ 'SessionStart',
22
+ 'UserPromptSubmit',
23
+ 'PreToolUse',
24
+ 'PostToolUse',
25
+ 'PostToolUseFailure',
26
+ 'Stop',
27
+ 'SessionEnd',
28
+ ];
29
+ for (const event of events) {
30
+ this.handlers.set(event, []);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Register a hook handler
36
+ */
37
+ register(handler: HookHandler): void {
38
+ const handlers = this.handlers.get(handler.event) || [];
39
+ handlers.push(handler);
40
+ this.handlers.set(handler.event, handlers);
41
+ }
42
+
43
+ /**
44
+ * Register multiple handlers from config
45
+ */
46
+ registerFromConfig(config: HooksConfig): void {
47
+ for (const [event, handlers] of Object.entries(config)) {
48
+ for (const handler of handlers || []) {
49
+ this.register({
50
+ ...handler,
51
+ event: event as HookEvent,
52
+ id: handler.id || `${event}-${Date.now()}`,
53
+ name: handler.name || handler.command || 'anonymous',
54
+ });
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Unregister a hook handler by ID
61
+ */
62
+ unregister(handlerId: string): boolean {
63
+ for (const [event, handlers] of this.handlers) {
64
+ const index = handlers.findIndex((h) => h.id === handlerId);
65
+ if (index !== -1) {
66
+ handlers.splice(index, 1);
67
+ this.handlers.set(event, handlers);
68
+ return true;
69
+ }
70
+ }
71
+ return false;
72
+ }
73
+
74
+ /**
75
+ * Execute all handlers for an event
76
+ */
77
+ async execute(event: HookEvent, input: HookInput): Promise<HookOutput[]> {
78
+ const handlers = this.handlers.get(event) || [];
79
+ const results: HookOutput[] = [];
80
+
81
+ for (const handler of handlers) {
82
+ // Check matcher if present
83
+ if (handler.matcher && 'toolName' in input) {
84
+ const pattern = typeof handler.matcher === 'string'
85
+ ? new RegExp(handler.matcher)
86
+ : handler.matcher;
87
+ if (!pattern.test(input.toolName)) {
88
+ continue;
89
+ }
90
+ }
91
+
92
+ try {
93
+ const result = await this.executeHandler(handler, input);
94
+ if (result) {
95
+ results.push(result);
96
+ // Stop processing if handler blocks
97
+ if (result.block) {
98
+ break;
99
+ }
100
+ }
101
+ } catch (error) {
102
+ console.error(`Hook error (${handler.name}):`, error);
103
+ }
104
+ }
105
+
106
+ return results;
107
+ }
108
+
109
+ /**
110
+ * Execute a single handler
111
+ */
112
+ private async executeHandler(
113
+ handler: HookHandler,
114
+ input: HookInput
115
+ ): Promise<HookOutput | void> {
116
+ const timeout = handler.timeout || 30000;
117
+
118
+ // If handler has inline function
119
+ if (handler.handler) {
120
+ return Promise.race([
121
+ handler.handler(input),
122
+ new Promise<void>((_, reject) =>
123
+ setTimeout(() => reject(new Error('Hook timeout')), timeout)
124
+ ),
125
+ ]);
126
+ }
127
+
128
+ // If handler has command
129
+ if (handler.command) {
130
+ return this.executeCommand(handler.command, input, timeout);
131
+ }
132
+
133
+ return undefined;
134
+ }
135
+
136
+ /**
137
+ * Execute a shell command hook
138
+ */
139
+ private executeCommand(
140
+ command: string,
141
+ input: HookInput,
142
+ timeout: number
143
+ ): Promise<HookOutput | void> {
144
+ return new Promise((resolve, reject) => {
145
+ const proc = spawn('sh', ['-c', command], {
146
+ env: {
147
+ ...process.env,
148
+ HOOK_INPUT: JSON.stringify(input),
149
+ },
150
+ timeout,
151
+ });
152
+
153
+ let stdout = '';
154
+ let stderr = '';
155
+
156
+ proc.stdout.on('data', (data) => {
157
+ stdout += data.toString();
158
+ });
159
+
160
+ proc.stderr.on('data', (data) => {
161
+ stderr += data.toString();
162
+ });
163
+
164
+ proc.on('close', (code) => {
165
+ if (code !== 0) {
166
+ // Non-zero exit = block action
167
+ resolve({
168
+ block: true,
169
+ blockReason: stderr || `Hook exited with code ${code}`,
170
+ });
171
+ } else {
172
+ // Try to parse JSON output
173
+ try {
174
+ if (stdout.trim()) {
175
+ resolve(JSON.parse(stdout) as HookOutput);
176
+ } else {
177
+ resolve(undefined);
178
+ }
179
+ } catch {
180
+ resolve(undefined);
181
+ }
182
+ }
183
+ });
184
+
185
+ proc.on('error', (err) => {
186
+ reject(err);
187
+ });
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Get all registered handlers
193
+ */
194
+ getHandlers(event?: HookEvent): HookHandler[] {
195
+ if (event) {
196
+ return this.handlers.get(event) || [];
197
+ }
198
+ return Array.from(this.handlers.values()).flat();
199
+ }
200
+
201
+ /**
202
+ * Clear all handlers
203
+ */
204
+ clear(): void {
205
+ for (const event of this.handlers.keys()) {
206
+ this.handlers.set(event, []);
207
+ }
208
+ }
209
+ }
210
+
211
+ // Singleton instance
212
+ export const hooksManager = new HooksManager();