@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,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,2 @@
1
+ export * from './manager.js';
2
+ export * from './storage.js';
@@ -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
+ }