@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.
- package/.eslintrc.json +36 -0
- package/.prettierrc +8 -0
- package/README.md +288 -0
- package/dist/cli/app.d.ts +12 -0
- package/dist/cli/app.d.ts.map +1 -0
- package/dist/cli/app.js +201 -0
- package/dist/cli/app.js.map +1 -0
- package/dist/cli/commands/index.d.ts +56 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +201 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/config/constants.d.ts +32 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +39 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +4 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +56 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +28 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/settings.d.ts +20 -0
- package/dist/config/settings.d.ts.map +1 -0
- package/dist/config/settings.js +102 -0
- package/dist/config/settings.js.map +1 -0
- package/dist/core/agents/agent.d.ts +33 -0
- package/dist/core/agents/agent.d.ts.map +1 -0
- package/dist/core/agents/agent.js +156 -0
- package/dist/core/agents/agent.js.map +1 -0
- package/dist/core/agents/index.d.ts +3 -0
- package/dist/core/agents/index.d.ts.map +1 -0
- package/dist/core/agents/index.js +3 -0
- package/dist/core/agents/index.js.map +1 -0
- package/dist/core/agents/orchestrator.d.ts +56 -0
- package/dist/core/agents/orchestrator.d.ts.map +1 -0
- package/dist/core/agents/orchestrator.js +151 -0
- package/dist/core/agents/orchestrator.js.map +1 -0
- package/dist/core/api/client.d.ts +46 -0
- package/dist/core/api/client.d.ts.map +1 -0
- package/dist/core/api/client.js +223 -0
- package/dist/core/api/client.js.map +1 -0
- package/dist/core/api/index.d.ts +3 -0
- package/dist/core/api/index.d.ts.map +1 -0
- package/dist/core/api/index.js +3 -0
- package/dist/core/api/index.js.map +1 -0
- package/dist/core/api/types.d.ts +126 -0
- package/dist/core/api/types.d.ts.map +1 -0
- package/dist/core/api/types.js +16 -0
- package/dist/core/api/types.js.map +1 -0
- package/dist/core/hooks/index.d.ts +3 -0
- package/dist/core/hooks/index.d.ts.map +1 -0
- package/dist/core/hooks/index.js +3 -0
- package/dist/core/hooks/index.js.map +1 -0
- package/dist/core/hooks/manager.d.ts +43 -0
- package/dist/core/hooks/manager.d.ts.map +1 -0
- package/dist/core/hooks/manager.js +178 -0
- package/dist/core/hooks/manager.js.map +1 -0
- package/dist/core/hooks/types.d.ts +68 -0
- package/dist/core/hooks/types.d.ts.map +1 -0
- package/dist/core/hooks/types.js +6 -0
- package/dist/core/hooks/types.js.map +1 -0
- package/dist/core/mcp/client.d.ts +48 -0
- package/dist/core/mcp/client.d.ts.map +1 -0
- package/dist/core/mcp/client.js +139 -0
- package/dist/core/mcp/client.js.map +1 -0
- package/dist/core/mcp/index.d.ts +3 -0
- package/dist/core/mcp/index.d.ts.map +1 -0
- package/dist/core/mcp/index.js +3 -0
- package/dist/core/mcp/index.js.map +1 -0
- package/dist/core/mcp/manager.d.ts +46 -0
- package/dist/core/mcp/manager.d.ts.map +1 -0
- package/dist/core/mcp/manager.js +133 -0
- package/dist/core/mcp/manager.js.map +1 -0
- package/dist/core/plugins/index.d.ts +3 -0
- package/dist/core/plugins/index.d.ts.map +1 -0
- package/dist/core/plugins/index.js +3 -0
- package/dist/core/plugins/index.js.map +1 -0
- package/dist/core/plugins/loader.d.ts +63 -0
- package/dist/core/plugins/loader.d.ts.map +1 -0
- package/dist/core/plugins/loader.js +258 -0
- package/dist/core/plugins/loader.js.map +1 -0
- package/dist/core/plugins/types.d.ts +95 -0
- package/dist/core/plugins/types.d.ts.map +1 -0
- package/dist/core/plugins/types.js +6 -0
- package/dist/core/plugins/types.js.map +1 -0
- package/dist/core/session/index.d.ts +3 -0
- package/dist/core/session/index.d.ts.map +1 -0
- package/dist/core/session/index.js +3 -0
- package/dist/core/session/index.js.map +1 -0
- package/dist/core/session/manager.d.ts +57 -0
- package/dist/core/session/manager.d.ts.map +1 -0
- package/dist/core/session/manager.js +182 -0
- package/dist/core/session/manager.js.map +1 -0
- package/dist/core/session/storage.d.ts +42 -0
- package/dist/core/session/storage.d.ts.map +1 -0
- package/dist/core/session/storage.js +138 -0
- package/dist/core/session/storage.js.map +1 -0
- package/dist/core/tools/definitions/file.d.ts +6 -0
- package/dist/core/tools/definitions/file.d.ts.map +1 -0
- package/dist/core/tools/definitions/file.js +276 -0
- package/dist/core/tools/definitions/file.js.map +1 -0
- package/dist/core/tools/definitions/git.d.ts +6 -0
- package/dist/core/tools/definitions/git.d.ts.map +1 -0
- package/dist/core/tools/definitions/git.js +294 -0
- package/dist/core/tools/definitions/git.js.map +1 -0
- package/dist/core/tools/definitions/index.d.ts +11 -0
- package/dist/core/tools/definitions/index.d.ts.map +1 -0
- package/dist/core/tools/definitions/index.js +22 -0
- package/dist/core/tools/definitions/index.js.map +1 -0
- package/dist/core/tools/definitions/search.d.ts +6 -0
- package/dist/core/tools/definitions/search.d.ts.map +1 -0
- package/dist/core/tools/definitions/search.js +223 -0
- package/dist/core/tools/definitions/search.js.map +1 -0
- package/dist/core/tools/definitions/shell.d.ts +6 -0
- package/dist/core/tools/definitions/shell.d.ts.map +1 -0
- package/dist/core/tools/definitions/shell.js +190 -0
- package/dist/core/tools/definitions/shell.js.map +1 -0
- package/dist/core/tools/definitions/web.d.ts +6 -0
- package/dist/core/tools/definitions/web.d.ts.map +1 -0
- package/dist/core/tools/definitions/web.js +104 -0
- package/dist/core/tools/definitions/web.js.map +1 -0
- package/dist/core/tools/executor.d.ts +24 -0
- package/dist/core/tools/executor.d.ts.map +1 -0
- package/dist/core/tools/executor.js +111 -0
- package/dist/core/tools/executor.js.map +1 -0
- package/dist/core/tools/index.d.ts +4 -0
- package/dist/core/tools/index.d.ts.map +1 -0
- package/dist/core/tools/index.js +4 -0
- package/dist/core/tools/index.js.map +1 -0
- package/dist/core/tools/registry.d.ts +23 -0
- package/dist/core/tools/registry.d.ts.map +1 -0
- package/dist/core/tools/registry.js +28 -0
- package/dist/core/tools/registry.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +352 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/src/cli/app.tsx +319 -0
- package/src/cli/commands/index.ts +261 -0
- package/src/config/constants.ts +45 -0
- package/src/config/index.ts +3 -0
- package/src/config/schema.ts +36 -0
- package/src/config/settings.ts +121 -0
- package/src/core/agents/agent.ts +205 -0
- package/src/core/agents/index.ts +2 -0
- package/src/core/agents/orchestrator.ts +223 -0
- package/src/core/api/client.ts +300 -0
- package/src/core/api/index.ts +2 -0
- package/src/core/api/types.ts +149 -0
- package/src/core/hooks/index.ts +2 -0
- package/src/core/hooks/manager.ts +212 -0
- package/src/core/hooks/types.ts +116 -0
- package/src/core/mcp/client.ts +198 -0
- package/src/core/mcp/index.ts +2 -0
- package/src/core/mcp/manager.ts +153 -0
- package/src/core/plugins/index.ts +2 -0
- package/src/core/plugins/loader.ts +312 -0
- package/src/core/plugins/types.ts +111 -0
- package/src/core/session/index.ts +2 -0
- package/src/core/session/manager.ts +246 -0
- package/src/core/session/storage.ts +184 -0
- package/src/core/tools/definitions/file.ts +312 -0
- package/src/core/tools/definitions/git.ts +326 -0
- package/src/core/tools/definitions/index.ts +24 -0
- package/src/core/tools/definitions/search.ts +266 -0
- package/src/core/tools/definitions/shell.ts +228 -0
- package/src/core/tools/definitions/web.ts +113 -0
- package/src/core/tools/executor.ts +145 -0
- package/src/core/tools/index.ts +3 -0
- package/src/core/tools/registry.ts +44 -0
- package/src/index.ts +407 -0
- package/tsconfig.json +40 -0
- 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,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,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();
|