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.
- package/README.md +32 -0
- package/dist/agent.d.ts +96 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +286 -0
- package/dist/hooks/executor.d.ts +56 -0
- package/dist/hooks/executor.d.ts.map +1 -0
- package/dist/hooks/executor.js +312 -0
- package/dist/hooks/index.d.ts +17 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +14 -0
- package/dist/hooks/manager.d.ts +90 -0
- package/dist/hooks/manager.d.ts.map +1 -0
- package/dist/hooks/manager.js +395 -0
- package/dist/hooks/matcher.d.ts +49 -0
- package/dist/hooks/matcher.d.ts.map +1 -0
- package/dist/hooks/matcher.js +147 -0
- package/dist/hooks/settings.d.ts +46 -0
- package/dist/hooks/settings.d.ts.map +1 -0
- package/dist/hooks/settings.js +100 -0
- package/dist/hooks/types.d.ts +80 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +59 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/managers/aiManager.d.ts +61 -0
- package/dist/managers/aiManager.d.ts.map +1 -0
- package/dist/managers/aiManager.js +415 -0
- package/dist/managers/backgroundBashManager.d.ts +27 -0
- package/dist/managers/backgroundBashManager.d.ts.map +1 -0
- package/dist/managers/backgroundBashManager.js +166 -0
- package/dist/managers/bashManager.d.ts +20 -0
- package/dist/managers/bashManager.d.ts.map +1 -0
- package/dist/managers/bashManager.js +66 -0
- package/dist/managers/mcpManager.d.ts +63 -0
- package/dist/managers/mcpManager.d.ts.map +1 -0
- package/dist/managers/mcpManager.js +378 -0
- package/dist/managers/messageManager.d.ts +85 -0
- package/dist/managers/messageManager.d.ts.map +1 -0
- package/dist/managers/messageManager.js +265 -0
- package/dist/managers/skillManager.d.ts +59 -0
- package/dist/managers/skillManager.d.ts.map +1 -0
- package/dist/managers/skillManager.js +317 -0
- package/dist/managers/slashCommandManager.d.ts +77 -0
- package/dist/managers/slashCommandManager.d.ts.map +1 -0
- package/dist/managers/slashCommandManager.js +208 -0
- package/dist/managers/toolManager.d.ts +23 -0
- package/dist/managers/toolManager.d.ts.map +1 -0
- package/dist/managers/toolManager.js +79 -0
- package/dist/services/aiService.d.ts +28 -0
- package/dist/services/aiService.d.ts.map +1 -0
- package/dist/services/aiService.js +180 -0
- package/dist/services/memory.d.ts +8 -0
- package/dist/services/memory.d.ts.map +1 -0
- package/dist/services/memory.js +128 -0
- package/dist/services/session.d.ts +54 -0
- package/dist/services/session.d.ts.map +1 -0
- package/dist/services/session.js +196 -0
- package/dist/tools/bashTool.d.ts +14 -0
- package/dist/tools/bashTool.d.ts.map +1 -0
- package/dist/tools/bashTool.js +351 -0
- package/dist/tools/deleteFileTool.d.ts +6 -0
- package/dist/tools/deleteFileTool.d.ts.map +1 -0
- package/dist/tools/deleteFileTool.js +67 -0
- package/dist/tools/editTool.d.ts +6 -0
- package/dist/tools/editTool.d.ts.map +1 -0
- package/dist/tools/editTool.js +168 -0
- package/dist/tools/globTool.d.ts +6 -0
- package/dist/tools/globTool.d.ts.map +1 -0
- package/dist/tools/globTool.js +113 -0
- package/dist/tools/grepTool.d.ts +6 -0
- package/dist/tools/grepTool.d.ts.map +1 -0
- package/dist/tools/grepTool.js +268 -0
- package/dist/tools/lsTool.d.ts +6 -0
- package/dist/tools/lsTool.d.ts.map +1 -0
- package/dist/tools/lsTool.js +160 -0
- package/dist/tools/multiEditTool.d.ts +6 -0
- package/dist/tools/multiEditTool.d.ts.map +1 -0
- package/dist/tools/multiEditTool.js +222 -0
- package/dist/tools/readTool.d.ts +6 -0
- package/dist/tools/readTool.d.ts.map +1 -0
- package/dist/tools/readTool.js +136 -0
- package/dist/tools/types.d.ts +35 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/writeTool.d.ts +6 -0
- package/dist/tools/writeTool.d.ts.map +1 -0
- package/dist/tools/writeTool.js +138 -0
- package/dist/types.d.ts +212 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/utils/bashHistory.d.ts +46 -0
- package/dist/utils/bashHistory.d.ts.map +1 -0
- package/dist/utils/bashHistory.js +236 -0
- package/dist/utils/commandArgumentParser.d.ts +34 -0
- package/dist/utils/commandArgumentParser.d.ts.map +1 -0
- package/dist/utils/commandArgumentParser.js +123 -0
- package/dist/utils/constants.d.ts +27 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +28 -0
- package/dist/utils/convertMessagesForAPI.d.ts +9 -0
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -0
- package/dist/utils/convertMessagesForAPI.js +189 -0
- package/dist/utils/customCommands.d.ts +14 -0
- package/dist/utils/customCommands.d.ts.map +1 -0
- package/dist/utils/customCommands.js +71 -0
- package/dist/utils/fileFilter.d.ts +26 -0
- package/dist/utils/fileFilter.d.ts.map +1 -0
- package/dist/utils/fileFilter.js +177 -0
- package/dist/utils/markdownParser.d.ts +27 -0
- package/dist/utils/markdownParser.d.ts.map +1 -0
- package/dist/utils/markdownParser.js +109 -0
- package/dist/utils/mcpUtils.d.ts +24 -0
- package/dist/utils/mcpUtils.d.ts.map +1 -0
- package/dist/utils/mcpUtils.js +51 -0
- package/dist/utils/messageOperations.d.ts +118 -0
- package/dist/utils/messageOperations.d.ts.map +1 -0
- package/dist/utils/messageOperations.js +334 -0
- package/dist/utils/path.d.ts +25 -0
- package/dist/utils/path.d.ts.map +1 -0
- package/dist/utils/path.js +109 -0
- package/dist/utils/skillParser.d.ts +18 -0
- package/dist/utils/skillParser.d.ts.map +1 -0
- package/dist/utils/skillParser.js +147 -0
- package/dist/utils/stringUtils.d.ts +13 -0
- package/dist/utils/stringUtils.d.ts.map +1 -0
- package/dist/utils/stringUtils.js +44 -0
- package/package.json +51 -0
- package/src/agent.ts +405 -0
- package/src/hooks/executor.ts +440 -0
- package/src/hooks/index.ts +52 -0
- package/src/hooks/manager.ts +618 -0
- package/src/hooks/matcher.ts +187 -0
- package/src/hooks/settings.ts +129 -0
- package/src/hooks/types.ts +169 -0
- package/src/index.ts +24 -0
- package/src/managers/aiManager.ts +573 -0
- package/src/managers/backgroundBashManager.ts +203 -0
- package/src/managers/bashManager.ts +97 -0
- package/src/managers/mcpManager.ts +493 -0
- package/src/managers/messageManager.ts +415 -0
- package/src/managers/skillManager.ts +404 -0
- package/src/managers/slashCommandManager.ts +293 -0
- package/src/managers/toolManager.ts +106 -0
- package/src/services/aiService.ts +252 -0
- package/src/services/memory.ts +149 -0
- package/src/services/session.ts +265 -0
- package/src/tools/bashTool.ts +402 -0
- package/src/tools/deleteFileTool.ts +81 -0
- package/src/tools/editTool.ts +192 -0
- package/src/tools/globTool.ts +135 -0
- package/src/tools/grepTool.ts +326 -0
- package/src/tools/lsTool.ts +187 -0
- package/src/tools/multiEditTool.ts +268 -0
- package/src/tools/readTool.ts +165 -0
- package/src/tools/types.ts +47 -0
- package/src/tools/writeTool.ts +163 -0
- package/src/types.ts +260 -0
- package/src/utils/bashHistory.ts +303 -0
- package/src/utils/commandArgumentParser.ts +153 -0
- package/src/utils/constants.ts +37 -0
- package/src/utils/convertMessagesForAPI.ts +236 -0
- package/src/utils/customCommands.ts +85 -0
- package/src/utils/fileFilter.ts +202 -0
- package/src/utils/markdownParser.ts +156 -0
- package/src/utils/mcpUtils.ts +81 -0
- package/src/utils/messageOperations.ts +506 -0
- package/src/utils/path.ts +118 -0
- package/src/utils/skillParser.ts +188 -0
- 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
|
+
}
|