@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,228 @@
1
+ /**
2
+ * Bluehawks CLI - Shell Command Tool
3
+ * Execute shell commands with safety controls
4
+ */
5
+
6
+ import { spawn } from 'node:child_process';
7
+ import { toolRegistry, type ToolHandler } from '../registry.js';
8
+ import { COMMAND_TIMEOUT_MS, MAX_OUTPUT_LENGTH } from '../../../config/constants.js';
9
+
10
+ // Dangerous command patterns
11
+ const DANGEROUS_PATTERNS = [
12
+ /\brm\s+-rf?\s+[\/~]/i,
13
+ /\bsudo\b/i,
14
+ /\bchmod\s+777\b/i,
15
+ /\bchown\b/i,
16
+ /\bmkfs\b/i,
17
+ /\bdd\s+if=/i,
18
+ />\s*\/dev\//i,
19
+ /\bshutdown\b/i,
20
+ /\breboot\b/i,
21
+ /\bhalt\b/i,
22
+ /\bpoweroff\b/i,
23
+ ];
24
+
25
+ function isDangerousCommand(command: string): { dangerous: boolean; reason?: string } {
26
+ for (const pattern of DANGEROUS_PATTERNS) {
27
+ if (pattern.test(command)) {
28
+ return {
29
+ dangerous: true,
30
+ reason: `Command matches dangerous pattern: ${pattern.source}`,
31
+ };
32
+ }
33
+ }
34
+ return { dangerous: false };
35
+ }
36
+
37
+ interface CommandResult {
38
+ stdout: string;
39
+ stderr: string;
40
+ exitCode: number | null;
41
+ signal: string | null;
42
+ }
43
+
44
+ async function executeCommand(
45
+ command: string,
46
+ cwd: string,
47
+ timeout: number
48
+ ): Promise<CommandResult> {
49
+ return new Promise((resolve, reject) => {
50
+ // Determine shell based on platform
51
+ const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/bash';
52
+ const shellArgs = process.platform === 'win32' ? ['/c', command] : ['-c', command];
53
+
54
+ const child = spawn(shell, shellArgs, {
55
+ cwd,
56
+ env: { ...process.env, TERM: 'dumb' },
57
+ stdio: ['pipe', 'pipe', 'pipe'],
58
+ });
59
+
60
+ let stdout = '';
61
+ let stderr = '';
62
+ let killed = false;
63
+
64
+ const timeoutId = setTimeout(() => {
65
+ killed = true;
66
+ child.kill('SIGTERM');
67
+ // Force kill after 5 seconds if still running
68
+ setTimeout(() => child.kill('SIGKILL'), 5000);
69
+ }, timeout);
70
+
71
+ child.stdout?.on('data', (data) => {
72
+ stdout += data.toString();
73
+ // Truncate if too long
74
+ if (stdout.length > MAX_OUTPUT_LENGTH) {
75
+ stdout = stdout.substring(0, MAX_OUTPUT_LENGTH) + '\n... (output truncated)';
76
+ child.kill('SIGTERM');
77
+ }
78
+ });
79
+
80
+ child.stderr?.on('data', (data) => {
81
+ stderr += data.toString();
82
+ if (stderr.length > MAX_OUTPUT_LENGTH) {
83
+ stderr = stderr.substring(0, MAX_OUTPUT_LENGTH) + '\n... (output truncated)';
84
+ }
85
+ });
86
+
87
+ child.on('close', (code, signal) => {
88
+ clearTimeout(timeoutId);
89
+ if (killed && !signal) {
90
+ reject(new Error('Command timed out'));
91
+ } else {
92
+ resolve({
93
+ stdout: stdout.trim(),
94
+ stderr: stderr.trim(),
95
+ exitCode: code,
96
+ signal: signal,
97
+ });
98
+ }
99
+ });
100
+
101
+ child.on('error', (error) => {
102
+ clearTimeout(timeoutId);
103
+ reject(error);
104
+ });
105
+ });
106
+ }
107
+
108
+ const runCommandTool: ToolHandler = {
109
+ name: 'run_command',
110
+ safeToAutoRun: false,
111
+ definition: {
112
+ type: 'function',
113
+ function: {
114
+ name: 'run_command',
115
+ description:
116
+ 'Execute a shell command. Use this for running tests, building projects, git commands, and other shell operations.',
117
+ parameters: {
118
+ type: 'object',
119
+ properties: {
120
+ command: {
121
+ type: 'string',
122
+ description: 'The shell command to execute.',
123
+ },
124
+ cwd: {
125
+ type: 'string',
126
+ description:
127
+ 'The working directory for the command. Defaults to the current directory.',
128
+ },
129
+ timeout: {
130
+ type: 'number',
131
+ description: `Timeout in milliseconds. Default is ${COMMAND_TIMEOUT_MS}ms.`,
132
+ },
133
+ },
134
+ required: ['command'],
135
+ },
136
+ },
137
+ },
138
+ async execute(args) {
139
+ const command = args.command as string;
140
+ const cwd = (args.cwd as string) || process.cwd();
141
+ const timeout = (args.timeout as number) || COMMAND_TIMEOUT_MS;
142
+
143
+ // Check for dangerous commands
144
+ const dangerCheck = isDangerousCommand(command);
145
+ if (dangerCheck.dangerous) {
146
+ throw new Error(`Dangerous command detected: ${dangerCheck.reason}`);
147
+ }
148
+
149
+ const result = await executeCommand(command, cwd, timeout);
150
+
151
+ // Format output
152
+ let output = '';
153
+
154
+ if (result.stdout) {
155
+ output += result.stdout;
156
+ }
157
+
158
+ if (result.stderr) {
159
+ if (output) output += '\n\n';
160
+ output += `STDERR:\n${result.stderr}`;
161
+ }
162
+
163
+ if (result.exitCode !== 0) {
164
+ output += `\n\nExit code: ${result.exitCode}`;
165
+ }
166
+
167
+ if (result.signal) {
168
+ output += `\n\nTerminated by signal: ${result.signal}`;
169
+ }
170
+
171
+ return output || '(no output)';
172
+ },
173
+ };
174
+
175
+ // Background command tool (for long-running processes)
176
+ const runBackgroundCommandTool: ToolHandler = {
177
+ name: 'run_background_command',
178
+ safeToAutoRun: false,
179
+ definition: {
180
+ type: 'function',
181
+ function: {
182
+ name: 'run_background_command',
183
+ description:
184
+ 'Start a background process like a dev server. The command will run in the background.',
185
+ parameters: {
186
+ type: 'object',
187
+ properties: {
188
+ command: {
189
+ type: 'string',
190
+ description: 'The shell command to run in the background.',
191
+ },
192
+ cwd: {
193
+ type: 'string',
194
+ description: 'The working directory for the command.',
195
+ },
196
+ },
197
+ required: ['command'],
198
+ },
199
+ },
200
+ },
201
+ async execute(args) {
202
+ const command = args.command as string;
203
+ const cwd = (args.cwd as string) || process.cwd();
204
+
205
+ const dangerCheck = isDangerousCommand(command);
206
+ if (dangerCheck.dangerous) {
207
+ throw new Error(`Dangerous command detected: ${dangerCheck.reason}`);
208
+ }
209
+
210
+ const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/bash';
211
+ const shellArgs = process.platform === 'win32' ? ['/c', command] : ['-c', command];
212
+
213
+ const child = spawn(shell, shellArgs, {
214
+ cwd,
215
+ detached: true,
216
+ stdio: 'ignore',
217
+ });
218
+
219
+ child.unref();
220
+
221
+ return `Started background process with PID: ${child.pid}\nCommand: ${command}`;
222
+ },
223
+ };
224
+
225
+ export function registerShellTools(): void {
226
+ toolRegistry.register(runCommandTool);
227
+ toolRegistry.register(runBackgroundCommandTool);
228
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Bluehawks CLI - Web Tools
3
+ * Tools for fetching web content
4
+ */
5
+
6
+ import { toolRegistry, type ToolHandler } from '../registry.js';
7
+ import { DEFAULT_TIMEOUT_MS } from '../../../config/constants.js';
8
+
9
+ const fetchUrlTool: ToolHandler = {
10
+ name: 'fetch_url',
11
+ safeToAutoRun: true,
12
+ definition: {
13
+ type: 'function',
14
+ function: {
15
+ name: 'fetch_url',
16
+ description:
17
+ 'Fetch content from a URL. Useful for reading documentation, API responses, or web pages.',
18
+ parameters: {
19
+ type: 'object',
20
+ properties: {
21
+ url: {
22
+ type: 'string',
23
+ description: 'The URL to fetch.',
24
+ },
25
+ method: {
26
+ type: 'string',
27
+ enum: ['GET', 'POST', 'PUT', 'DELETE'],
28
+ description: 'HTTP method. Default is GET.',
29
+ },
30
+ headers: {
31
+ type: 'object',
32
+ description: 'Optional HTTP headers as key-value pairs.',
33
+ },
34
+ body: {
35
+ type: 'string',
36
+ description: 'Optional request body for POST/PUT.',
37
+ },
38
+ timeout: {
39
+ type: 'number',
40
+ description: 'Timeout in milliseconds.',
41
+ },
42
+ },
43
+ required: ['url'],
44
+ },
45
+ },
46
+ },
47
+ async execute(args) {
48
+ const url = args.url as string;
49
+ const method = (args.method as string) || 'GET';
50
+ const headers = (args.headers as Record<string, string>) || {};
51
+ const body = args.body as string | undefined;
52
+ const timeout = (args.timeout as number) || DEFAULT_TIMEOUT_MS;
53
+
54
+ const controller = new AbortController();
55
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
56
+
57
+ try {
58
+ const response = await fetch(url, {
59
+ method,
60
+ headers: {
61
+ 'User-Agent': 'Bluehawks-CLI/1.0',
62
+ ...headers,
63
+ },
64
+ body: body && ['POST', 'PUT'].includes(method) ? body : undefined,
65
+ signal: controller.signal,
66
+ });
67
+
68
+ clearTimeout(timeoutId);
69
+
70
+ const contentType = response.headers.get('content-type') || '';
71
+ let content: string;
72
+
73
+ if (contentType.includes('application/json')) {
74
+ const json = await response.json();
75
+ content = JSON.stringify(json, null, 2);
76
+ } else {
77
+ content = await response.text();
78
+
79
+ // Simple HTML to text conversion
80
+ if (contentType.includes('text/html')) {
81
+ content = content
82
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
83
+ .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
84
+ .replace(/<[^>]+>/g, '\n')
85
+ .replace(/&nbsp;/g, ' ')
86
+ .replace(/&lt;/g, '<')
87
+ .replace(/&gt;/g, '>')
88
+ .replace(/&amp;/g, '&')
89
+ .replace(/\n\s*\n/g, '\n\n')
90
+ .trim();
91
+ }
92
+ }
93
+
94
+ // Truncate if too long
95
+ const maxLength = 50000;
96
+ if (content.length > maxLength) {
97
+ content = content.substring(0, maxLength) + '\n... (content truncated)';
98
+ }
99
+
100
+ return `Status: ${response.status} ${response.statusText}\n\n${content}`;
101
+ } catch (error) {
102
+ clearTimeout(timeoutId);
103
+ if (error instanceof Error && error.name === 'AbortError') {
104
+ throw new Error('Request timed out');
105
+ }
106
+ throw error;
107
+ }
108
+ },
109
+ };
110
+
111
+ export function registerWebTools(): void {
112
+ toolRegistry.register(fetchUrlTool);
113
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Bluehawks CLI - Tool Executor
3
+ * Executes tools with safety controls and output handling
4
+ */
5
+
6
+ import { toolRegistry, type ToolHandler } from './registry.js';
7
+ import type { ToolCall, ToolResult } from '../api/types.js';
8
+ import { MAX_OUTPUT_LENGTH } from '../../config/constants.js';
9
+
10
+ export type ApprovalMode = 'always' | 'never' | 'unsafe-only';
11
+
12
+ export interface ExecutorOptions {
13
+ approvalMode: ApprovalMode;
14
+ onApprovalRequest?: (toolName: string, args: Record<string, unknown>) => Promise<boolean>;
15
+ onToolStart?: (toolName: string, args: Record<string, unknown>) => void;
16
+ onToolEnd?: (toolName: string, result: string, isError: boolean) => void;
17
+ }
18
+
19
+ export class ToolExecutor {
20
+ private options: ExecutorOptions;
21
+
22
+ constructor(options: Partial<ExecutorOptions> = {}) {
23
+ this.options = {
24
+ approvalMode: options.approvalMode || 'unsafe-only',
25
+ onApprovalRequest: options.onApprovalRequest,
26
+ onToolStart: options.onToolStart,
27
+ onToolEnd: options.onToolEnd,
28
+ };
29
+ }
30
+
31
+ async executeToolCalls(toolCalls: ToolCall[]): Promise<ToolResult[]> {
32
+ const results: ToolResult[] = [];
33
+
34
+ for (const toolCall of toolCalls) {
35
+ const result = await this.executeToolCall(toolCall);
36
+ results.push(result);
37
+ }
38
+
39
+ return results;
40
+ }
41
+
42
+ async executeToolCall(toolCall: ToolCall): Promise<ToolResult> {
43
+ const { id, function: fn } = toolCall;
44
+ const { name, arguments: argsString } = fn;
45
+
46
+ let args: Record<string, unknown>;
47
+ try {
48
+ args = JSON.parse(argsString);
49
+ } catch {
50
+ return {
51
+ tool_call_id: id,
52
+ content: `Error: Failed to parse tool arguments: ${argsString}`,
53
+ isError: true,
54
+ };
55
+ }
56
+
57
+ const handler = toolRegistry.get(name);
58
+ if (!handler) {
59
+ return {
60
+ tool_call_id: id,
61
+ content: `Error: Unknown tool: ${name}`,
62
+ isError: true,
63
+ };
64
+ }
65
+
66
+ // Check if approval is needed
67
+ const needsApproval = await this.needsApproval(handler);
68
+ if (needsApproval) {
69
+ const approved = await this.requestApproval(name, args);
70
+ if (!approved) {
71
+ return {
72
+ tool_call_id: id,
73
+ content: 'Tool execution was denied by the user.',
74
+ isError: true,
75
+ };
76
+ }
77
+ }
78
+
79
+ // Execute the tool
80
+ this.options.onToolStart?.(name, args);
81
+
82
+ try {
83
+ let result = await handler.execute(args);
84
+
85
+ // Truncate output if too long
86
+ if (result.length > MAX_OUTPUT_LENGTH) {
87
+ result = result.substring(0, MAX_OUTPUT_LENGTH) + '\n... (output truncated)';
88
+ }
89
+
90
+ this.options.onToolEnd?.(name, result, false);
91
+
92
+ return {
93
+ tool_call_id: id,
94
+ content: result,
95
+ isError: false,
96
+ };
97
+ } catch (error) {
98
+ const errorMessage = error instanceof Error ? error.message : String(error);
99
+ this.options.onToolEnd?.(name, errorMessage, true);
100
+
101
+ return {
102
+ tool_call_id: id,
103
+ content: `Error: ${errorMessage}`,
104
+ isError: true,
105
+ };
106
+ }
107
+ }
108
+
109
+ private async needsApproval(handler: ToolHandler): Promise<boolean> {
110
+ switch (this.options.approvalMode) {
111
+ case 'never':
112
+ return false;
113
+ case 'always':
114
+ return true;
115
+ case 'unsafe-only':
116
+ return !handler.safeToAutoRun;
117
+ default:
118
+ return !handler.safeToAutoRun;
119
+ }
120
+ }
121
+
122
+ private async requestApproval(
123
+ toolName: string,
124
+ args: Record<string, unknown>
125
+ ): Promise<boolean> {
126
+ if (!this.options.onApprovalRequest) {
127
+ // If no approval handler, deny by default
128
+ return false;
129
+ }
130
+
131
+ return this.options.onApprovalRequest(toolName, args);
132
+ }
133
+
134
+ setApprovalMode(mode: ApprovalMode): void {
135
+ this.options.approvalMode = mode;
136
+ }
137
+
138
+ setApprovalHandler(
139
+ handler: (toolName: string, args: Record<string, unknown>) => Promise<boolean>
140
+ ): void {
141
+ this.options.onApprovalRequest = handler;
142
+ }
143
+ }
144
+
145
+ export const toolExecutor = new ToolExecutor();
@@ -0,0 +1,3 @@
1
+ export * from './registry.js';
2
+ export * from './executor.js';
3
+ export * from './definitions/index.js';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Bluehawks CLI - Tool Registry
3
+ * Central registry for all available tools
4
+ */
5
+
6
+ import type { ToolDefinition } from '../api/types.js';
7
+
8
+ export interface ToolHandler {
9
+ name: string;
10
+ definition: ToolDefinition;
11
+ execute: (args: Record<string, unknown>) => Promise<string>;
12
+ safeToAutoRun: boolean;
13
+ }
14
+
15
+ class ToolRegistry {
16
+ private tools: Map<string, ToolHandler> = new Map();
17
+
18
+ register(handler: ToolHandler): void {
19
+ this.tools.set(handler.name, handler);
20
+ }
21
+
22
+ get(name: string): ToolHandler | undefined {
23
+ return this.tools.get(name);
24
+ }
25
+
26
+ getAll(): ToolHandler[] {
27
+ return Array.from(this.tools.values());
28
+ }
29
+
30
+ getDefinitions(): ToolDefinition[] {
31
+ return this.getAll().map((handler) => handler.definition);
32
+ }
33
+
34
+ has(name: string): boolean {
35
+ return this.tools.has(name);
36
+ }
37
+
38
+ isSafeToAutoRun(name: string): boolean {
39
+ const handler = this.tools.get(name);
40
+ return handler?.safeToAutoRun ?? false;
41
+ }
42
+ }
43
+
44
+ export const toolRegistry = new ToolRegistry();