@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,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bluehawks CLI - Plugin Loader
|
|
3
|
+
* Discovers and loads plugins from plugin directories
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'node:fs/promises';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import type {
|
|
11
|
+
PluginManifest,
|
|
12
|
+
LoadedPlugin,
|
|
13
|
+
PluginDiscovery,
|
|
14
|
+
PluginCommandHandler,
|
|
15
|
+
PluginToolHandler,
|
|
16
|
+
PluginContext,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
import { toolRegistry } from '../tools/index.js';
|
|
19
|
+
import { hooksManager } from '../hooks/index.js';
|
|
20
|
+
import type { ToolDefinition } from '../api/types.js';
|
|
21
|
+
|
|
22
|
+
// Plugin directories to search
|
|
23
|
+
const PLUGIN_DIRS = [
|
|
24
|
+
path.join(os.homedir(), '.bluehawks', 'plugins'),
|
|
25
|
+
path.join(process.cwd(), '.bluehawks', 'plugins'),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export class PluginLoader {
|
|
29
|
+
private plugins: Map<string, LoadedPlugin> = new Map();
|
|
30
|
+
private commandHandlers: Map<string, PluginCommandHandler> = new Map();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Discover all plugins in plugin directories
|
|
34
|
+
*/
|
|
35
|
+
async discover(): Promise<PluginDiscovery[]> {
|
|
36
|
+
const discoveries: PluginDiscovery[] = [];
|
|
37
|
+
|
|
38
|
+
for (const pluginDir of PLUGIN_DIRS) {
|
|
39
|
+
try {
|
|
40
|
+
const entries = await fs.readdir(pluginDir, { withFileTypes: true });
|
|
41
|
+
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (!entry.isDirectory()) continue;
|
|
44
|
+
|
|
45
|
+
const pluginPath = path.join(pluginDir, entry.name);
|
|
46
|
+
const manifestPath = path.join(pluginPath, 'plugin.json');
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
|
50
|
+
const manifest = JSON.parse(manifestContent) as PluginManifest;
|
|
51
|
+
discoveries.push({ path: pluginPath, manifest });
|
|
52
|
+
} catch {
|
|
53
|
+
// No valid manifest, skip
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Directory doesn't exist, skip
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return discoveries;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Load all discovered plugins
|
|
66
|
+
*/
|
|
67
|
+
async loadAll(): Promise<void> {
|
|
68
|
+
const discoveries = await this.discover();
|
|
69
|
+
|
|
70
|
+
for (const discovery of discoveries) {
|
|
71
|
+
try {
|
|
72
|
+
await this.load(discovery);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(`Failed to load plugin ${discovery.manifest.name}:`, error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Load a single plugin
|
|
81
|
+
*/
|
|
82
|
+
async load(discovery: PluginDiscovery): Promise<LoadedPlugin> {
|
|
83
|
+
const { manifest, path: pluginPath } = discovery;
|
|
84
|
+
|
|
85
|
+
const loaded: LoadedPlugin = {
|
|
86
|
+
manifest,
|
|
87
|
+
path: pluginPath,
|
|
88
|
+
commands: new Map(),
|
|
89
|
+
tools: new Map(),
|
|
90
|
+
hooks: [],
|
|
91
|
+
agents: new Map(),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Load commands
|
|
95
|
+
if (manifest.commands) {
|
|
96
|
+
for (const cmd of manifest.commands) {
|
|
97
|
+
const handler = await this.loadCommandHandler(pluginPath, cmd.handler);
|
|
98
|
+
if (handler) {
|
|
99
|
+
loaded.commands.set(cmd.name, handler);
|
|
100
|
+
this.commandHandlers.set(`/${cmd.name}`, handler);
|
|
101
|
+
|
|
102
|
+
// Register aliases
|
|
103
|
+
if (cmd.aliases) {
|
|
104
|
+
for (const alias of cmd.aliases) {
|
|
105
|
+
this.commandHandlers.set(`/${alias}`, handler);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Load tools
|
|
113
|
+
if (manifest.tools) {
|
|
114
|
+
for (const tool of manifest.tools) {
|
|
115
|
+
const handler = await this.loadToolHandler(pluginPath, tool.handler);
|
|
116
|
+
if (handler) {
|
|
117
|
+
loaded.tools.set(tool.name, handler);
|
|
118
|
+
this.registerPluginTool(tool.name, tool.description, tool.parameters, handler, tool.safeToAutoRun);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Load hooks
|
|
124
|
+
if (manifest.hooks) {
|
|
125
|
+
for (const hook of manifest.hooks) {
|
|
126
|
+
const hookHandler = await this.createHookHandler(pluginPath, hook, manifest.name);
|
|
127
|
+
loaded.hooks.push(hookHandler);
|
|
128
|
+
hooksManager.register(hookHandler);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Load agents
|
|
133
|
+
if (manifest.agents) {
|
|
134
|
+
for (const agent of manifest.agents) {
|
|
135
|
+
loaded.agents.set(agent.name, agent);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.plugins.set(manifest.name, loaded);
|
|
140
|
+
return loaded;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Load a command handler from file
|
|
145
|
+
*/
|
|
146
|
+
private async loadCommandHandler(
|
|
147
|
+
pluginPath: string,
|
|
148
|
+
handlerPath?: string
|
|
149
|
+
): Promise<PluginCommandHandler | null> {
|
|
150
|
+
if (!handlerPath) return null;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const fullPath = path.join(pluginPath, handlerPath);
|
|
154
|
+
const module = await import(pathToFileURL(fullPath).href);
|
|
155
|
+
return module.default || module.handler;
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Load a tool handler from file
|
|
163
|
+
*/
|
|
164
|
+
private async loadToolHandler(
|
|
165
|
+
pluginPath: string,
|
|
166
|
+
handlerPath: string
|
|
167
|
+
): Promise<PluginToolHandler | null> {
|
|
168
|
+
try {
|
|
169
|
+
const fullPath = path.join(pluginPath, handlerPath);
|
|
170
|
+
const module = await import(pathToFileURL(fullPath).href);
|
|
171
|
+
return module.default || module.handler;
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a hook handler from plugin config
|
|
179
|
+
*/
|
|
180
|
+
private async createHookHandler(
|
|
181
|
+
pluginPath: string,
|
|
182
|
+
hook: import('./types.js').PluginHook,
|
|
183
|
+
pluginName: string
|
|
184
|
+
) {
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
186
|
+
const hookHandler: any = {
|
|
187
|
+
id: `${pluginName}-${hook.event}-${Date.now()}`,
|
|
188
|
+
name: `${pluginName}:${hook.event}`,
|
|
189
|
+
event: hook.event,
|
|
190
|
+
async: hook.async,
|
|
191
|
+
timeout: hook.timeout,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (hook.matcher) {
|
|
195
|
+
hookHandler.matcher = new RegExp(hook.matcher);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (hook.command) {
|
|
199
|
+
hookHandler.command = hook.command;
|
|
200
|
+
} else if (hook.handler) {
|
|
201
|
+
try {
|
|
202
|
+
const fullPath = path.join(pluginPath, hook.handler);
|
|
203
|
+
const module = await import(pathToFileURL(fullPath).href);
|
|
204
|
+
hookHandler.handler = module.default || module.handler;
|
|
205
|
+
} catch {
|
|
206
|
+
// Handler load failed
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return hookHandler;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Register a plugin tool with the tool registry
|
|
215
|
+
*/
|
|
216
|
+
private registerPluginTool(
|
|
217
|
+
name: string,
|
|
218
|
+
description: string,
|
|
219
|
+
parameters: ToolDefinition['function']['parameters'],
|
|
220
|
+
handler: PluginToolHandler,
|
|
221
|
+
safeToAutoRun = false
|
|
222
|
+
): void {
|
|
223
|
+
const definition: ToolDefinition = {
|
|
224
|
+
type: 'function',
|
|
225
|
+
function: {
|
|
226
|
+
name: `plugin_${name}`,
|
|
227
|
+
description: `[Plugin] ${description}`,
|
|
228
|
+
parameters,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
toolRegistry.register({
|
|
233
|
+
name: `plugin_${name}`,
|
|
234
|
+
definition,
|
|
235
|
+
execute: async (args: Record<string, unknown>) => {
|
|
236
|
+
const context: PluginContext = {
|
|
237
|
+
projectPath: process.cwd(),
|
|
238
|
+
sessionId: `session_${Date.now()}`,
|
|
239
|
+
model: process.env.BLUEHAWKS_MODEL || 'unknown',
|
|
240
|
+
};
|
|
241
|
+
return handler(args, context);
|
|
242
|
+
},
|
|
243
|
+
safeToAutoRun,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Execute a plugin command
|
|
249
|
+
*/
|
|
250
|
+
async executeCommand(
|
|
251
|
+
commandName: string,
|
|
252
|
+
args: string[],
|
|
253
|
+
context: PluginContext
|
|
254
|
+
): Promise<string | null> {
|
|
255
|
+
const handler = this.commandHandlers.get(commandName);
|
|
256
|
+
if (!handler) return null;
|
|
257
|
+
|
|
258
|
+
const result = await handler(args, context);
|
|
259
|
+
return result || '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check if a command is a plugin command
|
|
264
|
+
*/
|
|
265
|
+
hasCommand(commandName: string): boolean {
|
|
266
|
+
return this.commandHandlers.has(commandName);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get all loaded plugins
|
|
271
|
+
*/
|
|
272
|
+
getPlugins(): LoadedPlugin[] {
|
|
273
|
+
return Array.from(this.plugins.values());
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get a specific plugin
|
|
278
|
+
*/
|
|
279
|
+
getPlugin(name: string): LoadedPlugin | undefined {
|
|
280
|
+
return this.plugins.get(name);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get all plugin agents
|
|
285
|
+
*/
|
|
286
|
+
getAgents(): Map<string, LoadedPlugin['agents']> {
|
|
287
|
+
const allAgents = new Map<string, LoadedPlugin['agents']>();
|
|
288
|
+
for (const [name, plugin] of this.plugins) {
|
|
289
|
+
if (plugin.agents.size > 0) {
|
|
290
|
+
allAgents.set(name, plugin.agents);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return allAgents;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Unload all plugins
|
|
298
|
+
*/
|
|
299
|
+
unloadAll(): void {
|
|
300
|
+
for (const plugin of this.plugins.values()) {
|
|
301
|
+
// Remove hooks
|
|
302
|
+
for (const hook of plugin.hooks) {
|
|
303
|
+
hooksManager.unregister(hook.id);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
this.plugins.clear();
|
|
307
|
+
this.commandHandlers.clear();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Singleton instance
|
|
312
|
+
export const pluginLoader = new PluginLoader();
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bluehawks CLI - Plugin Types
|
|
3
|
+
* Type definitions for the plugin system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ToolDefinition } from '../api/types.js';
|
|
7
|
+
import type { HookHandler } from '../hooks/types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Plugin manifest (plugin.json)
|
|
11
|
+
*/
|
|
12
|
+
export interface PluginManifest {
|
|
13
|
+
name: string;
|
|
14
|
+
version: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
author?: string;
|
|
17
|
+
main?: string; // Entry point for JS plugins
|
|
18
|
+
commands?: PluginCommand[];
|
|
19
|
+
tools?: PluginTool[];
|
|
20
|
+
hooks?: PluginHook[];
|
|
21
|
+
agents?: PluginAgent[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Custom slash command definition
|
|
26
|
+
*/
|
|
27
|
+
export interface PluginCommand {
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
aliases?: string[];
|
|
31
|
+
handler?: string; // Path to handler file
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Custom tool definition
|
|
36
|
+
*/
|
|
37
|
+
export interface PluginTool {
|
|
38
|
+
name: string;
|
|
39
|
+
description: string;
|
|
40
|
+
parameters: ToolDefinition['function']['parameters'];
|
|
41
|
+
handler: string; // Path to handler file
|
|
42
|
+
safeToAutoRun?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Hook configuration
|
|
47
|
+
*/
|
|
48
|
+
export interface PluginHook {
|
|
49
|
+
event: string;
|
|
50
|
+
command?: string;
|
|
51
|
+
handler?: string; // Path to handler file
|
|
52
|
+
matcher?: string;
|
|
53
|
+
async?: boolean;
|
|
54
|
+
timeout?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Custom agent definition
|
|
59
|
+
*/
|
|
60
|
+
export interface PluginAgent {
|
|
61
|
+
name: string;
|
|
62
|
+
description: string;
|
|
63
|
+
systemPrompt: string;
|
|
64
|
+
tools?: string[];
|
|
65
|
+
maxIterations?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Loaded plugin instance
|
|
70
|
+
*/
|
|
71
|
+
export interface LoadedPlugin {
|
|
72
|
+
manifest: PluginManifest;
|
|
73
|
+
path: string;
|
|
74
|
+
commands: Map<string, PluginCommandHandler>;
|
|
75
|
+
tools: Map<string, PluginToolHandler>;
|
|
76
|
+
hooks: HookHandler[];
|
|
77
|
+
agents: Map<string, PluginAgent>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Command handler function
|
|
82
|
+
*/
|
|
83
|
+
export type PluginCommandHandler = (
|
|
84
|
+
args: string[],
|
|
85
|
+
context: PluginContext
|
|
86
|
+
) => Promise<string | void>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Tool handler function
|
|
90
|
+
*/
|
|
91
|
+
export type PluginToolHandler = (
|
|
92
|
+
args: Record<string, unknown>,
|
|
93
|
+
context: PluginContext
|
|
94
|
+
) => Promise<string>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Context passed to plugin handlers
|
|
98
|
+
*/
|
|
99
|
+
export interface PluginContext {
|
|
100
|
+
projectPath: string;
|
|
101
|
+
sessionId: string;
|
|
102
|
+
model: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Plugin discovery result
|
|
107
|
+
*/
|
|
108
|
+
export interface PluginDiscovery {
|
|
109
|
+
path: string;
|
|
110
|
+
manifest: PluginManifest;
|
|
111
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bluehawks CLI - Session Manager
|
|
3
|
+
* Manages conversation history and session state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'node:fs/promises';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import type { Message } from '../api/types.js';
|
|
9
|
+
import { CONFIG_DIR_NAME, HISTORY_FILE, MAX_HISTORY_MESSAGES } from '../../config/constants.js';
|
|
10
|
+
|
|
11
|
+
export interface Session {
|
|
12
|
+
id: string;
|
|
13
|
+
startTime: Date;
|
|
14
|
+
messages: Message[];
|
|
15
|
+
metadata: {
|
|
16
|
+
projectPath: string;
|
|
17
|
+
model: string;
|
|
18
|
+
tokensUsed: number;
|
|
19
|
+
toolsUsed: string[];
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SessionStats {
|
|
24
|
+
messageCount: number;
|
|
25
|
+
userMessages: number;
|
|
26
|
+
assistantMessages: number;
|
|
27
|
+
toolMessages: number;
|
|
28
|
+
tokensUsed: number;
|
|
29
|
+
toolsUsed: string[];
|
|
30
|
+
duration: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class SessionManager {
|
|
34
|
+
private session: Session;
|
|
35
|
+
private configDir: string;
|
|
36
|
+
|
|
37
|
+
constructor(projectPath: string, model: string) {
|
|
38
|
+
this.configDir = path.join(projectPath, CONFIG_DIR_NAME);
|
|
39
|
+
this.session = {
|
|
40
|
+
id: this.generateSessionId(),
|
|
41
|
+
startTime: new Date(),
|
|
42
|
+
messages: [],
|
|
43
|
+
metadata: {
|
|
44
|
+
projectPath,
|
|
45
|
+
model,
|
|
46
|
+
tokensUsed: 0,
|
|
47
|
+
toolsUsed: [],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private generateSessionId(): string {
|
|
53
|
+
return `session_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
addMessage(message: Message): void {
|
|
57
|
+
this.session.messages.push(message);
|
|
58
|
+
|
|
59
|
+
// Limit history size
|
|
60
|
+
if (this.session.messages.length > MAX_HISTORY_MESSAGES) {
|
|
61
|
+
this.compressHistory();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
addMessages(messages: Message[]): void {
|
|
66
|
+
for (const message of messages) {
|
|
67
|
+
this.addMessage(message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
addToolUsed(toolName: string): void {
|
|
72
|
+
if (!this.session.metadata.toolsUsed.includes(toolName)) {
|
|
73
|
+
this.session.metadata.toolsUsed.push(toolName);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
addTokensUsed(tokens: number): void {
|
|
78
|
+
this.session.metadata.tokensUsed += tokens;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getMessages(): Message[] {
|
|
82
|
+
return [...this.session.messages];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getStats(): SessionStats {
|
|
86
|
+
const messages = this.session.messages;
|
|
87
|
+
const duration = Date.now() - this.session.startTime.getTime();
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
messageCount: messages.length,
|
|
91
|
+
userMessages: messages.filter((m) => m.role === 'user').length,
|
|
92
|
+
assistantMessages: messages.filter((m) => m.role === 'assistant').length,
|
|
93
|
+
toolMessages: messages.filter((m) => m.role === 'tool').length,
|
|
94
|
+
tokensUsed: this.session.metadata.tokensUsed,
|
|
95
|
+
toolsUsed: [...this.session.metadata.toolsUsed],
|
|
96
|
+
duration,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
clear(): void {
|
|
101
|
+
this.session.messages = [];
|
|
102
|
+
this.session.metadata.tokensUsed = 0;
|
|
103
|
+
this.session.metadata.toolsUsed = [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
compressHistory(): void {
|
|
107
|
+
// Keep system message and recent messages
|
|
108
|
+
const systemMessage = this.session.messages.find((m) => m.role === 'system');
|
|
109
|
+
const recentMessages = this.session.messages.slice(-20);
|
|
110
|
+
|
|
111
|
+
// Create a summary of older messages
|
|
112
|
+
const olderMessages = this.session.messages.slice(
|
|
113
|
+
systemMessage ? 1 : 0,
|
|
114
|
+
-20
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (olderMessages.length > 0) {
|
|
118
|
+
const summaryContent = `[Previous conversation compressed: ${olderMessages.length} messages removed to save context. Key topics discussed included: ${this.extractTopics(olderMessages)}]`;
|
|
119
|
+
|
|
120
|
+
const newMessages: Message[] = [];
|
|
121
|
+
if (systemMessage) {
|
|
122
|
+
newMessages.push(systemMessage);
|
|
123
|
+
}
|
|
124
|
+
newMessages.push({
|
|
125
|
+
role: 'assistant',
|
|
126
|
+
content: summaryContent,
|
|
127
|
+
});
|
|
128
|
+
newMessages.push(...recentMessages);
|
|
129
|
+
|
|
130
|
+
this.session.messages = newMessages;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private extractTopics(messages: Message[]): string {
|
|
135
|
+
// Simple topic extraction - just get first few words from user messages
|
|
136
|
+
const userMessages = messages
|
|
137
|
+
.filter((m) => m.role === 'user')
|
|
138
|
+
.map((m) => {
|
|
139
|
+
const content = typeof m.content === 'string' ? m.content : '';
|
|
140
|
+
return content.substring(0, 50).replace(/\n/g, ' ');
|
|
141
|
+
})
|
|
142
|
+
.slice(0, 5);
|
|
143
|
+
|
|
144
|
+
return userMessages.join(', ') || 'general coding assistance';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async save(name?: string): Promise<string> {
|
|
148
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
149
|
+
|
|
150
|
+
// Save to local project config
|
|
151
|
+
const historyPath = path.join(this.configDir, HISTORY_FILE);
|
|
152
|
+
const data = JSON.stringify(this.session, null, 2);
|
|
153
|
+
await fs.writeFile(historyPath, data, 'utf-8');
|
|
154
|
+
|
|
155
|
+
// Also save to global session storage for --continue/--resume
|
|
156
|
+
const { sessionStorage } = await import('./storage.js');
|
|
157
|
+
const preview = this.getPreview();
|
|
158
|
+
await sessionStorage.saveSession(
|
|
159
|
+
this.session.id,
|
|
160
|
+
name || null,
|
|
161
|
+
this.session,
|
|
162
|
+
{
|
|
163
|
+
projectPath: this.session.metadata.projectPath,
|
|
164
|
+
model: this.session.metadata.model,
|
|
165
|
+
messageCount: this.session.messages.length,
|
|
166
|
+
preview,
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return historyPath;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get a preview of the session (first user message)
|
|
175
|
+
*/
|
|
176
|
+
private getPreview(): string {
|
|
177
|
+
const userMessage = this.session.messages.find(m => m.role === 'user');
|
|
178
|
+
if (userMessage && typeof userMessage.content === 'string') {
|
|
179
|
+
return userMessage.content.substring(0, 100).replace(/\n/g, ' ');
|
|
180
|
+
}
|
|
181
|
+
return '';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Load from global session storage (for --continue/--resume)
|
|
186
|
+
*/
|
|
187
|
+
async loadFromGlobalStorage(sessionIdOrName?: string): Promise<boolean> {
|
|
188
|
+
const { sessionStorage } = await import('./storage.js');
|
|
189
|
+
|
|
190
|
+
let sessionData: unknown;
|
|
191
|
+
if (sessionIdOrName) {
|
|
192
|
+
sessionData = await sessionStorage.loadSession(sessionIdOrName);
|
|
193
|
+
} else {
|
|
194
|
+
const last = await sessionStorage.loadLastSession();
|
|
195
|
+
sessionData = last?.data;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (sessionData && typeof sessionData === 'object' && sessionData !== null) {
|
|
199
|
+
const loaded = sessionData as Session;
|
|
200
|
+
this.session = {
|
|
201
|
+
...loaded,
|
|
202
|
+
startTime: new Date(loaded.startTime),
|
|
203
|
+
};
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Set a name for the current session
|
|
211
|
+
*/
|
|
212
|
+
setSessionName(name: string): void {
|
|
213
|
+
// Store the name - will be used when saving
|
|
214
|
+
(this.session as Session & { name?: string }).name = name;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
async load(sessionId?: string): Promise<boolean> {
|
|
219
|
+
const historyPath = path.join(this.configDir, HISTORY_FILE);
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const data = await fs.readFile(historyPath, 'utf-8');
|
|
223
|
+
const loaded = JSON.parse(data) as Session;
|
|
224
|
+
|
|
225
|
+
if (!sessionId || loaded.id === sessionId) {
|
|
226
|
+
this.session = {
|
|
227
|
+
...loaded,
|
|
228
|
+
startTime: new Date(loaded.startTime),
|
|
229
|
+
};
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// No history file or invalid format
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
getSessionId(): string {
|
|
240
|
+
return this.session.id;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getStartTime(): Date {
|
|
244
|
+
return this.session.startTime;
|
|
245
|
+
}
|
|
246
|
+
}
|