wave-agent-sdk 0.0.1

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 (170) hide show
  1. package/README.md +32 -0
  2. package/dist/agent.d.ts +96 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +286 -0
  5. package/dist/hooks/executor.d.ts +56 -0
  6. package/dist/hooks/executor.d.ts.map +1 -0
  7. package/dist/hooks/executor.js +312 -0
  8. package/dist/hooks/index.d.ts +17 -0
  9. package/dist/hooks/index.d.ts.map +1 -0
  10. package/dist/hooks/index.js +14 -0
  11. package/dist/hooks/manager.d.ts +90 -0
  12. package/dist/hooks/manager.d.ts.map +1 -0
  13. package/dist/hooks/manager.js +395 -0
  14. package/dist/hooks/matcher.d.ts +49 -0
  15. package/dist/hooks/matcher.d.ts.map +1 -0
  16. package/dist/hooks/matcher.js +147 -0
  17. package/dist/hooks/settings.d.ts +46 -0
  18. package/dist/hooks/settings.d.ts.map +1 -0
  19. package/dist/hooks/settings.js +100 -0
  20. package/dist/hooks/types.d.ts +80 -0
  21. package/dist/hooks/types.d.ts.map +1 -0
  22. package/dist/hooks/types.js +59 -0
  23. package/dist/index.d.ts +16 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +20 -0
  26. package/dist/managers/aiManager.d.ts +61 -0
  27. package/dist/managers/aiManager.d.ts.map +1 -0
  28. package/dist/managers/aiManager.js +415 -0
  29. package/dist/managers/backgroundBashManager.d.ts +27 -0
  30. package/dist/managers/backgroundBashManager.d.ts.map +1 -0
  31. package/dist/managers/backgroundBashManager.js +166 -0
  32. package/dist/managers/bashManager.d.ts +20 -0
  33. package/dist/managers/bashManager.d.ts.map +1 -0
  34. package/dist/managers/bashManager.js +66 -0
  35. package/dist/managers/mcpManager.d.ts +63 -0
  36. package/dist/managers/mcpManager.d.ts.map +1 -0
  37. package/dist/managers/mcpManager.js +378 -0
  38. package/dist/managers/messageManager.d.ts +85 -0
  39. package/dist/managers/messageManager.d.ts.map +1 -0
  40. package/dist/managers/messageManager.js +265 -0
  41. package/dist/managers/skillManager.d.ts +59 -0
  42. package/dist/managers/skillManager.d.ts.map +1 -0
  43. package/dist/managers/skillManager.js +317 -0
  44. package/dist/managers/slashCommandManager.d.ts +77 -0
  45. package/dist/managers/slashCommandManager.d.ts.map +1 -0
  46. package/dist/managers/slashCommandManager.js +208 -0
  47. package/dist/managers/toolManager.d.ts +23 -0
  48. package/dist/managers/toolManager.d.ts.map +1 -0
  49. package/dist/managers/toolManager.js +79 -0
  50. package/dist/services/aiService.d.ts +28 -0
  51. package/dist/services/aiService.d.ts.map +1 -0
  52. package/dist/services/aiService.js +180 -0
  53. package/dist/services/memory.d.ts +8 -0
  54. package/dist/services/memory.d.ts.map +1 -0
  55. package/dist/services/memory.js +128 -0
  56. package/dist/services/session.d.ts +54 -0
  57. package/dist/services/session.d.ts.map +1 -0
  58. package/dist/services/session.js +196 -0
  59. package/dist/tools/bashTool.d.ts +14 -0
  60. package/dist/tools/bashTool.d.ts.map +1 -0
  61. package/dist/tools/bashTool.js +351 -0
  62. package/dist/tools/deleteFileTool.d.ts +6 -0
  63. package/dist/tools/deleteFileTool.d.ts.map +1 -0
  64. package/dist/tools/deleteFileTool.js +67 -0
  65. package/dist/tools/editTool.d.ts +6 -0
  66. package/dist/tools/editTool.d.ts.map +1 -0
  67. package/dist/tools/editTool.js +168 -0
  68. package/dist/tools/globTool.d.ts +6 -0
  69. package/dist/tools/globTool.d.ts.map +1 -0
  70. package/dist/tools/globTool.js +113 -0
  71. package/dist/tools/grepTool.d.ts +6 -0
  72. package/dist/tools/grepTool.d.ts.map +1 -0
  73. package/dist/tools/grepTool.js +268 -0
  74. package/dist/tools/lsTool.d.ts +6 -0
  75. package/dist/tools/lsTool.d.ts.map +1 -0
  76. package/dist/tools/lsTool.js +160 -0
  77. package/dist/tools/multiEditTool.d.ts +6 -0
  78. package/dist/tools/multiEditTool.d.ts.map +1 -0
  79. package/dist/tools/multiEditTool.js +222 -0
  80. package/dist/tools/readTool.d.ts +6 -0
  81. package/dist/tools/readTool.d.ts.map +1 -0
  82. package/dist/tools/readTool.js +136 -0
  83. package/dist/tools/types.d.ts +35 -0
  84. package/dist/tools/types.d.ts.map +1 -0
  85. package/dist/tools/types.js +4 -0
  86. package/dist/tools/writeTool.d.ts +6 -0
  87. package/dist/tools/writeTool.d.ts.map +1 -0
  88. package/dist/tools/writeTool.js +138 -0
  89. package/dist/types.d.ts +212 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +13 -0
  92. package/dist/utils/bashHistory.d.ts +46 -0
  93. package/dist/utils/bashHistory.d.ts.map +1 -0
  94. package/dist/utils/bashHistory.js +236 -0
  95. package/dist/utils/commandArgumentParser.d.ts +34 -0
  96. package/dist/utils/commandArgumentParser.d.ts.map +1 -0
  97. package/dist/utils/commandArgumentParser.js +123 -0
  98. package/dist/utils/constants.d.ts +27 -0
  99. package/dist/utils/constants.d.ts.map +1 -0
  100. package/dist/utils/constants.js +28 -0
  101. package/dist/utils/convertMessagesForAPI.d.ts +9 -0
  102. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -0
  103. package/dist/utils/convertMessagesForAPI.js +189 -0
  104. package/dist/utils/customCommands.d.ts +14 -0
  105. package/dist/utils/customCommands.d.ts.map +1 -0
  106. package/dist/utils/customCommands.js +71 -0
  107. package/dist/utils/fileFilter.d.ts +26 -0
  108. package/dist/utils/fileFilter.d.ts.map +1 -0
  109. package/dist/utils/fileFilter.js +177 -0
  110. package/dist/utils/markdownParser.d.ts +27 -0
  111. package/dist/utils/markdownParser.d.ts.map +1 -0
  112. package/dist/utils/markdownParser.js +109 -0
  113. package/dist/utils/mcpUtils.d.ts +24 -0
  114. package/dist/utils/mcpUtils.d.ts.map +1 -0
  115. package/dist/utils/mcpUtils.js +51 -0
  116. package/dist/utils/messageOperations.d.ts +118 -0
  117. package/dist/utils/messageOperations.d.ts.map +1 -0
  118. package/dist/utils/messageOperations.js +334 -0
  119. package/dist/utils/path.d.ts +25 -0
  120. package/dist/utils/path.d.ts.map +1 -0
  121. package/dist/utils/path.js +109 -0
  122. package/dist/utils/skillParser.d.ts +18 -0
  123. package/dist/utils/skillParser.d.ts.map +1 -0
  124. package/dist/utils/skillParser.js +147 -0
  125. package/dist/utils/stringUtils.d.ts +13 -0
  126. package/dist/utils/stringUtils.d.ts.map +1 -0
  127. package/dist/utils/stringUtils.js +44 -0
  128. package/package.json +51 -0
  129. package/src/agent.ts +405 -0
  130. package/src/hooks/executor.ts +440 -0
  131. package/src/hooks/index.ts +52 -0
  132. package/src/hooks/manager.ts +618 -0
  133. package/src/hooks/matcher.ts +187 -0
  134. package/src/hooks/settings.ts +129 -0
  135. package/src/hooks/types.ts +169 -0
  136. package/src/index.ts +24 -0
  137. package/src/managers/aiManager.ts +573 -0
  138. package/src/managers/backgroundBashManager.ts +203 -0
  139. package/src/managers/bashManager.ts +97 -0
  140. package/src/managers/mcpManager.ts +493 -0
  141. package/src/managers/messageManager.ts +415 -0
  142. package/src/managers/skillManager.ts +404 -0
  143. package/src/managers/slashCommandManager.ts +293 -0
  144. package/src/managers/toolManager.ts +106 -0
  145. package/src/services/aiService.ts +252 -0
  146. package/src/services/memory.ts +149 -0
  147. package/src/services/session.ts +265 -0
  148. package/src/tools/bashTool.ts +402 -0
  149. package/src/tools/deleteFileTool.ts +81 -0
  150. package/src/tools/editTool.ts +192 -0
  151. package/src/tools/globTool.ts +135 -0
  152. package/src/tools/grepTool.ts +326 -0
  153. package/src/tools/lsTool.ts +187 -0
  154. package/src/tools/multiEditTool.ts +268 -0
  155. package/src/tools/readTool.ts +165 -0
  156. package/src/tools/types.ts +47 -0
  157. package/src/tools/writeTool.ts +163 -0
  158. package/src/types.ts +260 -0
  159. package/src/utils/bashHistory.ts +303 -0
  160. package/src/utils/commandArgumentParser.ts +153 -0
  161. package/src/utils/constants.ts +37 -0
  162. package/src/utils/convertMessagesForAPI.ts +236 -0
  163. package/src/utils/customCommands.ts +85 -0
  164. package/src/utils/fileFilter.ts +202 -0
  165. package/src/utils/markdownParser.ts +156 -0
  166. package/src/utils/mcpUtils.ts +81 -0
  167. package/src/utils/messageOperations.ts +506 -0
  168. package/src/utils/path.ts +118 -0
  169. package/src/utils/skillParser.ts +188 -0
  170. package/src/utils/stringUtils.ts +50 -0
@@ -0,0 +1,404 @@
1
+ import { readdir, stat } from "fs/promises";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import type {
5
+ SkillManagerOptions,
6
+ SkillMetadata,
7
+ Skill,
8
+ SkillCollection,
9
+ SkillDiscoveryResult,
10
+ SkillToolArgs,
11
+ SkillInvocationContext,
12
+ Logger,
13
+ } from "../types.js";
14
+ import type { ToolPlugin, ToolResult } from "../tools/types.js";
15
+ import { parseSkillFile, formatSkillError } from "../utils/skillParser.js";
16
+
17
+ /**
18
+ * Manages skill discovery and loading
19
+ */
20
+ export class SkillManager {
21
+ private personalSkillsPath: string;
22
+ private scanTimeout: number;
23
+ private logger?: Logger;
24
+
25
+ private skillMetadata = new Map<string, SkillMetadata>();
26
+ private skillContent = new Map<string, Skill>();
27
+ private initialized = false;
28
+
29
+ constructor(options: SkillManagerOptions = {}) {
30
+ this.personalSkillsPath =
31
+ options.personalSkillsPath || join(homedir(), ".wave", "skills");
32
+ this.scanTimeout = options.scanTimeout || 5000;
33
+ this.logger = options.logger;
34
+ }
35
+
36
+ /**
37
+ * Initialize the skill manager by discovering available skills
38
+ */
39
+ async initialize(): Promise<void> {
40
+ this.logger?.info("Initializing SkillManager...");
41
+
42
+ try {
43
+ // Clear existing data before discovery
44
+ this.skillMetadata.clear();
45
+ this.skillContent.clear();
46
+
47
+ const discovery = await this.discoverSkills();
48
+
49
+ // Store discovered skill metadata
50
+ discovery.personalSkills.forEach((skill, name) => {
51
+ this.skillMetadata.set(name, skill);
52
+ });
53
+ discovery.projectSkills.forEach((skill, name) => {
54
+ this.skillMetadata.set(name, skill);
55
+ });
56
+
57
+ // Log any discovery errors
58
+ if (discovery.errors.length > 0) {
59
+ this.logger?.warn(
60
+ `Found ${discovery.errors.length} skill discovery errors`,
61
+ );
62
+ discovery.errors.forEach((error) => {
63
+ this.logger?.warn(
64
+ `Skill error in ${error.skillPath}: ${error.message}`,
65
+ );
66
+ });
67
+ }
68
+
69
+ this.initialized = true;
70
+ this.logger?.info(
71
+ `SkillManager initialized with ${this.skillMetadata.size} skills`,
72
+ );
73
+ } catch (error) {
74
+ this.logger?.error("Failed to initialize SkillManager:", error);
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get all available skills metadata
81
+ */
82
+ getAvailableSkills(): SkillMetadata[] {
83
+ if (!this.initialized) {
84
+ throw new Error("SkillManager not initialized. Call initialize() first.");
85
+ }
86
+
87
+ return Array.from(this.skillMetadata.values());
88
+ }
89
+
90
+ /**
91
+ * Load a specific skill by name
92
+ * Returns the skill content that was loaded during initialization
93
+ */
94
+ async loadSkill(skillName: string): Promise<Skill | null> {
95
+ if (!this.initialized) {
96
+ throw new Error("SkillManager not initialized. Call initialize() first.");
97
+ }
98
+
99
+ // Return skill content that was loaded during initialization
100
+ const skill = this.skillContent.get(skillName);
101
+ if (skill) {
102
+ this.logger?.debug(`Skill '${skillName}' retrieved from loaded content`);
103
+ return skill;
104
+ }
105
+
106
+ this.logger?.debug(`Skill '${skillName}' not found`);
107
+ return null;
108
+ }
109
+
110
+ /**
111
+ * Discover skills in both personal and project directories
112
+ */
113
+ private async discoverSkills(): Promise<SkillDiscoveryResult> {
114
+ const personalCollection = await this.discoverSkillCollection(
115
+ this.personalSkillsPath,
116
+ "personal",
117
+ );
118
+
119
+ const projectCollection = await this.discoverSkillCollection(
120
+ process.cwd(),
121
+ "project",
122
+ );
123
+
124
+ return {
125
+ personalSkills: personalCollection.skills,
126
+ projectSkills: projectCollection.skills,
127
+ errors: [...personalCollection.errors, ...projectCollection.errors],
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Discover skills in a specific directory
133
+ */
134
+ private async discoverSkillCollection(
135
+ basePath: string,
136
+ type: "personal" | "project",
137
+ ): Promise<SkillCollection> {
138
+ const collection: SkillCollection = {
139
+ type,
140
+ basePath,
141
+ skills: new Map(),
142
+ errors: [],
143
+ };
144
+
145
+ const skillsPath =
146
+ type === "personal" ? basePath : join(basePath, ".wave", "skills");
147
+
148
+ try {
149
+ const skillDirs = await this.findSkillDirectories(skillsPath);
150
+ this.logger?.debug(
151
+ `Found ${skillDirs.length} potential skill directories in ${skillsPath}`,
152
+ );
153
+
154
+ for (const skillDir of skillDirs) {
155
+ try {
156
+ const skillFilePath = join(skillDir, "SKILL.md");
157
+
158
+ // Check if SKILL.md exists
159
+ try {
160
+ await stat(skillFilePath);
161
+ } catch {
162
+ continue; // Skip directories without SKILL.md
163
+ }
164
+
165
+ const parsed = parseSkillFile(skillFilePath, {
166
+ basePath: skillDir,
167
+ validateMetadata: true,
168
+ });
169
+
170
+ if (parsed.isValid) {
171
+ // Override the skill type with the collection type
172
+ const skillMetadata = {
173
+ ...parsed.skillMetadata,
174
+ type,
175
+ };
176
+
177
+ // Create full skill object with content
178
+ const skill: Skill = {
179
+ name: parsed.skillMetadata.name,
180
+ description: parsed.skillMetadata.description,
181
+ type: type, // Use the collection type
182
+ skillPath: parsed.skillMetadata.skillPath,
183
+ content: parsed.content,
184
+ frontmatter: parsed.frontmatter,
185
+ isValid: parsed.isValid,
186
+ errors: parsed.validationErrors,
187
+ };
188
+
189
+ collection.skills.set(skillMetadata.name, skillMetadata);
190
+ // Store the full skill content in the manager's skillContent map
191
+ this.skillContent.set(skillMetadata.name, skill);
192
+ } else {
193
+ collection.errors.push({
194
+ skillPath: skillDir,
195
+ message: parsed.validationErrors.join("; "),
196
+ });
197
+ }
198
+ } catch (error) {
199
+ collection.errors.push({
200
+ skillPath: skillDir,
201
+ message: `Failed to process skill: ${error instanceof Error ? error.message : String(error)}`,
202
+ });
203
+ }
204
+ }
205
+ } catch (error) {
206
+ this.logger?.debug(
207
+ `Could not scan ${skillsPath}: ${error instanceof Error ? error.message : String(error)}`,
208
+ );
209
+ // Not an error - the directory might not exist yet
210
+ }
211
+
212
+ return collection;
213
+ }
214
+
215
+ /**
216
+ * Find all directories that could contain skills
217
+ */
218
+ private async findSkillDirectories(skillsPath: string): Promise<string[]> {
219
+ const directories: string[] = [];
220
+
221
+ try {
222
+ const entries = await readdir(skillsPath, { withFileTypes: true });
223
+
224
+ for (const entry of entries) {
225
+ if (entry.isDirectory()) {
226
+ directories.push(join(skillsPath, entry.name));
227
+ }
228
+ }
229
+ } catch {
230
+ // Directory doesn't exist - return empty array
231
+ }
232
+
233
+ return directories;
234
+ }
235
+
236
+ /**
237
+ * Create a tool plugin for registering with ToolManager
238
+ */
239
+ createTool(): ToolPlugin {
240
+ // Initialize skill manager asynchronously
241
+ let initializationPromise: Promise<void> | null = null;
242
+
243
+ const ensureInitialized = async (): Promise<void> => {
244
+ if (!initializationPromise) {
245
+ initializationPromise = this.initialize();
246
+ }
247
+ await initializationPromise;
248
+ };
249
+
250
+ const getToolDescription = (): string => {
251
+ if (!this.initialized) {
252
+ return "Invoke a Wave skill by name. Skills are user-defined automation templates that can be personal or project-specific. Skills will be loaded during initialization.";
253
+ }
254
+
255
+ const availableSkills = this.getAvailableSkills();
256
+
257
+ if (availableSkills.length === 0) {
258
+ return "Invoke a Wave skill by name. Skills are user-defined automation templates that can be personal or project-specific. No skills are currently available.";
259
+ }
260
+
261
+ const skillList = availableSkills
262
+ .map(
263
+ (skill) =>
264
+ `• **${skill.name}** (${skill.type}): ${skill.description}`,
265
+ )
266
+ .join("\n");
267
+
268
+ return `Invoke a Wave skill by name. Skills are user-defined automation templates that can be personal or project-specific.\n\nAvailable skills:\n${skillList}`;
269
+ };
270
+
271
+ return {
272
+ name: "skill",
273
+ config: {
274
+ type: "function",
275
+ function: {
276
+ name: "skill",
277
+ description: getToolDescription(),
278
+ parameters: {
279
+ type: "object",
280
+ properties: {
281
+ skill_name: {
282
+ type: "string",
283
+ description: "Name of the skill to invoke",
284
+ enum: this.initialized
285
+ ? this.getAvailableSkills().map((skill) => skill.name)
286
+ : [],
287
+ },
288
+ },
289
+ required: ["skill_name"],
290
+ },
291
+ },
292
+ },
293
+ execute: async (args: Record<string, unknown>): Promise<ToolResult> => {
294
+ try {
295
+ // Ensure skill manager is initialized
296
+ await ensureInitialized();
297
+
298
+ // Validate arguments
299
+ const skillName = args.skill_name as string;
300
+ if (!skillName || typeof skillName !== "string") {
301
+ return {
302
+ success: false,
303
+ content: "",
304
+ error: "skill_name parameter is required and must be a string",
305
+ };
306
+ }
307
+
308
+ // Execute the skill
309
+ const result = await this.executeSkill({ skill_name: skillName });
310
+
311
+ return {
312
+ success: true,
313
+ content: result.content,
314
+ shortResult: `Invoked skill: ${skillName}`,
315
+ };
316
+ } catch (error) {
317
+ return {
318
+ success: false,
319
+ content: "",
320
+ error: error instanceof Error ? error.message : String(error),
321
+ };
322
+ }
323
+ },
324
+ formatCompactParams: (params: Record<string, unknown>) => {
325
+ const skillName = params.skill_name as string;
326
+ return skillName || "unknown-skill";
327
+ },
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Execute a skill by name
333
+ */
334
+ async executeSkill(
335
+ args: SkillToolArgs,
336
+ ): Promise<{ content: string; context?: SkillInvocationContext }> {
337
+ const { skill_name } = args;
338
+
339
+ this.logger?.info(`Invoking skill: ${skill_name}`);
340
+
341
+ try {
342
+ // Load the skill
343
+ const skill = await this.loadSkill(skill_name);
344
+
345
+ if (!skill) {
346
+ return {
347
+ content: `❌ **Skill not found**: "${skill_name}"\n\nAvailable skills:\n${this.formatAvailableSkills()}`,
348
+ };
349
+ }
350
+
351
+ if (!skill.isValid) {
352
+ const errorMsg = formatSkillError(skill.skillPath, skill.errors);
353
+ return {
354
+ content: `❌ **Skill validation failed**:\n\n\`\`\`\n${errorMsg}\n\`\`\``,
355
+ };
356
+ }
357
+
358
+ // Return skill content with context
359
+ return {
360
+ content: this.formatSkillContent(skill),
361
+ context: {
362
+ skillName: skill_name,
363
+ },
364
+ };
365
+ } catch (error) {
366
+ this.logger?.error(`Failed to execute skill '${skill_name}':`, error);
367
+ return {
368
+ content: `❌ **Error executing skill**: ${error instanceof Error ? error.message : String(error)}`,
369
+ };
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Format skill content for output
375
+ */
376
+ private formatSkillContent(skill: Skill): string {
377
+ const header = `🧠 **${skill.name}** (${skill.type} skill)\n\n`;
378
+ const description = `*${skill.description}*\n\n`;
379
+ const skillPath = `📁 Skill location: \`${skill.skillPath}\`\n\n`;
380
+
381
+ // Extract content after frontmatter
382
+ const contentMatch = skill.content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
383
+ const mainContent = contentMatch ? contentMatch[1].trim() : skill.content;
384
+
385
+ return header + description + skillPath + mainContent;
386
+ }
387
+
388
+ /**
389
+ * Format available skills list for error messages
390
+ */
391
+ private formatAvailableSkills(): string {
392
+ const skills = this.getAvailableSkills();
393
+
394
+ if (skills.length === 0) {
395
+ return "• No skills available\n\nTo create skills, see the Wave Skills documentation.";
396
+ }
397
+
398
+ return skills
399
+ .map(
400
+ (skill) => `• **${skill.name}** (${skill.type}): ${skill.description}`,
401
+ )
402
+ .join("\n");
403
+ }
404
+ }
@@ -0,0 +1,293 @@
1
+ import type { MessageManager } from "./messageManager.js";
2
+ import type { AIManager } from "./aiManager.js";
3
+ import type { SlashCommand, CustomSlashCommand, Logger } from "../types.js";
4
+ import { loadCustomSlashCommands } from "../utils/customCommands.js";
5
+
6
+ import {
7
+ substituteCommandParameters,
8
+ parseSlashCommandInput,
9
+ hasParameterPlaceholders,
10
+ } from "../utils/commandArgumentParser.js";
11
+ import {
12
+ BashCommandResult,
13
+ parseBashCommands,
14
+ replaceBashCommandsWithOutput,
15
+ } from "../utils/markdownParser.js";
16
+ import { exec } from "child_process";
17
+ import { promisify } from "util";
18
+
19
+ const execAsync = promisify(exec);
20
+
21
+ export interface SlashCommandManagerOptions {
22
+ messageManager: MessageManager;
23
+ aiManager: AIManager;
24
+ workdir: string;
25
+ logger?: Logger;
26
+ }
27
+
28
+ export class SlashCommandManager {
29
+ private commands = new Map<string, SlashCommand>();
30
+ private customCommands = new Map<string, CustomSlashCommand>();
31
+ private messageManager: MessageManager;
32
+ private aiManager: AIManager;
33
+ private workdir: string;
34
+ private logger?: Logger;
35
+
36
+ constructor(options: SlashCommandManagerOptions) {
37
+ this.messageManager = options.messageManager;
38
+ this.aiManager = options.aiManager;
39
+ this.workdir = options.workdir;
40
+ this.logger = options.logger;
41
+
42
+ this.initializeBuiltinCommands();
43
+ this.loadCustomCommands();
44
+ }
45
+
46
+ private initializeBuiltinCommands(): void {
47
+ // Register built-in clear command
48
+ this.registerCommand({
49
+ id: "clear",
50
+ name: "clear",
51
+ description: "Clear the chat session",
52
+ handler: () => {
53
+ this.messageManager.clearMessages();
54
+ },
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Load custom commands from filesystem
60
+ */
61
+ private loadCustomCommands(): void {
62
+ try {
63
+ const customCommands = loadCustomSlashCommands(this.workdir);
64
+
65
+ for (const command of customCommands) {
66
+ this.customCommands.set(command.id, command);
67
+
68
+ // Generate description: prioritize custom description, otherwise use default description
69
+ const description =
70
+ command.description ||
71
+ `Custom command: ${command.name}${hasParameterPlaceholders(command.content) ? " (supports parameters)" : ""}`;
72
+
73
+ // Register as a regular command with a handler that executes the custom command
74
+ this.registerCommand({
75
+ id: command.id,
76
+ name: command.name,
77
+ description,
78
+ handler: async (args?: string) => {
79
+ // Substitute parameters in the command content
80
+ const processedContent =
81
+ hasParameterPlaceholders(command.content) && args
82
+ ? substituteCommandParameters(command.content, args)
83
+ : command.content;
84
+
85
+ await this.executeCustomCommandInMainAgent(
86
+ command.name,
87
+ processedContent,
88
+ command.config,
89
+ args,
90
+ );
91
+ },
92
+ });
93
+ }
94
+
95
+ this.logger?.info(`Loaded ${customCommands.length} custom commands`);
96
+ } catch (error) {
97
+ this.logger?.warn("Failed to load custom commands:", error);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Reload custom commands (useful for development)
103
+ */
104
+ public reloadCustomCommands(): void {
105
+ // Clear existing custom commands
106
+ for (const commandId of this.customCommands.keys()) {
107
+ this.unregisterCommand(commandId);
108
+ }
109
+ this.customCommands.clear();
110
+
111
+ // Reload
112
+ this.loadCustomCommands();
113
+ }
114
+
115
+ /**
116
+ * Register new command
117
+ */
118
+ private registerCommand(command: SlashCommand): void {
119
+ this.commands.set(command.id, command);
120
+ }
121
+
122
+ /**
123
+ * Unregister command
124
+ */
125
+ private unregisterCommand(commandId: string): boolean {
126
+ return this.commands.delete(commandId);
127
+ }
128
+
129
+ /**
130
+ * Get all available commands
131
+ */
132
+ public getCommands(): SlashCommand[] {
133
+ return Array.from(this.commands.values());
134
+ }
135
+
136
+ /**
137
+ * Get command by ID
138
+ */
139
+ public getCommand(commandId: string): SlashCommand | undefined {
140
+ return this.commands.get(commandId);
141
+ }
142
+
143
+ /**
144
+ * Execute command
145
+ */
146
+ public async executeCommand(
147
+ commandId: string,
148
+ args?: string,
149
+ ): Promise<boolean> {
150
+ const command = this.commands.get(commandId);
151
+ if (!command) {
152
+ return false;
153
+ }
154
+
155
+ try {
156
+ await command.handler(args);
157
+ return true;
158
+ } catch (error) {
159
+ console.error(`Failed to execute slash command ${commandId}:`, error);
160
+ return false;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Parse and validate a slash command input
166
+ * Returns whether the command is valid along with parsed commandId and args
167
+ */
168
+ public parseAndValidateSlashCommand(input: string): {
169
+ isValid: boolean;
170
+ commandId?: string;
171
+ args?: string;
172
+ } {
173
+ try {
174
+ const { command: commandId, args } = parseSlashCommandInput(input);
175
+ const isValid = this.hasCommand(commandId);
176
+ return {
177
+ isValid,
178
+ commandId: isValid ? commandId : undefined,
179
+ args: isValid ? args || undefined : undefined, // Convert empty string to undefined
180
+ };
181
+ } catch (error) {
182
+ console.error(`Failed to parse slash command input "${input}":`, error);
183
+ return { isValid: false };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Check if command exists
189
+ */
190
+ public hasCommand(commandId: string): boolean {
191
+ return this.commands.has(commandId);
192
+ }
193
+
194
+ /**
195
+ * Get custom command details
196
+ */
197
+ public getCustomCommand(commandId: string): CustomSlashCommand | undefined {
198
+ return this.customCommands.get(commandId);
199
+ }
200
+
201
+ /**
202
+ * Get all custom commands
203
+ */
204
+ public getCustomCommands(): CustomSlashCommand[] {
205
+ return Array.from(this.customCommands.values());
206
+ }
207
+
208
+ /**
209
+ * Execute custom command in main agent instead of sub-agent
210
+ */
211
+ private async executeCustomCommandInMainAgent(
212
+ commandName: string,
213
+ content: string,
214
+ config?: { model?: string; allowedTools?: string[] },
215
+ args?: string,
216
+ ): Promise<void> {
217
+ try {
218
+ // Parse bash commands from the content
219
+ const { commands, processedContent } = parseBashCommands(content);
220
+
221
+ // Execute bash commands if any
222
+ const bashResults: BashCommandResult[] = [];
223
+ for (const command of commands) {
224
+ try {
225
+ const { stdout, stderr } = await execAsync(command, {
226
+ cwd: this.workdir,
227
+ timeout: 30000, // 30 second timeout
228
+ });
229
+ bashResults.push({
230
+ command,
231
+ output: stdout || stderr || "",
232
+ exitCode: 0,
233
+ });
234
+ } catch (error) {
235
+ const execError = error as {
236
+ stdout?: string;
237
+ stderr?: string;
238
+ message?: string;
239
+ code?: number;
240
+ };
241
+ bashResults.push({
242
+ command,
243
+ output:
244
+ execError.stdout || execError.stderr || execError.message || "",
245
+ exitCode: execError.code || 1,
246
+ });
247
+ }
248
+ }
249
+
250
+ // Replace bash command placeholders with their outputs
251
+ const finalContent =
252
+ bashResults.length > 0
253
+ ? replaceBashCommandsWithOutput(processedContent, bashResults)
254
+ : processedContent;
255
+
256
+ // Add custom command block to show the command being executed
257
+ const originalInput = args
258
+ ? `/${commandName} ${args}`
259
+ : `/${commandName}`;
260
+ this.messageManager.addCustomCommandMessage(
261
+ commandName,
262
+ finalContent,
263
+ originalInput,
264
+ );
265
+
266
+ // Execute the AI conversation with custom configuration
267
+ await this.aiManager.sendAIMessage({
268
+ model: config?.model,
269
+ allowedTools: config?.allowedTools,
270
+ });
271
+ } catch (error) {
272
+ this.logger?.error(
273
+ `Failed to execute custom command '${commandName}':`,
274
+ error,
275
+ );
276
+
277
+ // Add error to message manager
278
+ this.messageManager.addErrorBlock(
279
+ `Failed to execute custom command '${commandName}': ${
280
+ error instanceof Error ? error.message : String(error)
281
+ }`,
282
+ );
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Interrupt the currently executing slash command
288
+ */
289
+ public abortCurrentCommand(): void {
290
+ // Abort the AI manager if it's running
291
+ this.aiManager.abortAIMessage();
292
+ }
293
+ }