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
package/src/types.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { ChildProcess } from "child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Logger interface definition
|
|
5
|
+
* Compatible with OpenAI package Logger interface
|
|
6
|
+
*/
|
|
7
|
+
export interface Logger {
|
|
8
|
+
error: (...args: unknown[]) => void;
|
|
9
|
+
warn: (...args: unknown[]) => void;
|
|
10
|
+
info: (...args: unknown[]) => void;
|
|
11
|
+
debug: (...args: unknown[]) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Message {
|
|
15
|
+
role: "user" | "assistant";
|
|
16
|
+
blocks: MessageBlock[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type MessageBlock =
|
|
20
|
+
| TextBlock
|
|
21
|
+
| ErrorBlock
|
|
22
|
+
| ToolBlock
|
|
23
|
+
| ImageBlock
|
|
24
|
+
| DiffBlock
|
|
25
|
+
| CommandOutputBlock
|
|
26
|
+
| CompressBlock
|
|
27
|
+
| MemoryBlock
|
|
28
|
+
| CustomCommandBlock;
|
|
29
|
+
|
|
30
|
+
export interface TextBlock {
|
|
31
|
+
type: "text";
|
|
32
|
+
content: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ErrorBlock {
|
|
36
|
+
type: "error";
|
|
37
|
+
content: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ToolBlock {
|
|
41
|
+
type: "tool";
|
|
42
|
+
parameters?: string;
|
|
43
|
+
result?: string;
|
|
44
|
+
shortResult?: string; // Add shortResult field
|
|
45
|
+
images?: Array<{
|
|
46
|
+
// Add image data support
|
|
47
|
+
data: string; // Base64 encoded image data
|
|
48
|
+
mediaType?: string; // Media type of the image
|
|
49
|
+
}>;
|
|
50
|
+
id?: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
isRunning?: boolean; // Mark if tool is actually executing
|
|
53
|
+
success?: boolean;
|
|
54
|
+
error?: string | Error;
|
|
55
|
+
compactParams?: string; // Compact parameter display
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ImageBlock {
|
|
59
|
+
type: "image";
|
|
60
|
+
imageUrls?: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DiffBlock {
|
|
64
|
+
type: "diff";
|
|
65
|
+
path: string;
|
|
66
|
+
diffResult: Array<{
|
|
67
|
+
value: string;
|
|
68
|
+
added?: boolean;
|
|
69
|
+
removed?: boolean;
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface CommandOutputBlock {
|
|
74
|
+
type: "command_output";
|
|
75
|
+
command: string;
|
|
76
|
+
output: string;
|
|
77
|
+
isRunning: boolean;
|
|
78
|
+
exitCode: number | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CompressBlock {
|
|
82
|
+
type: "compress";
|
|
83
|
+
content: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface MemoryBlock {
|
|
87
|
+
type: "memory";
|
|
88
|
+
content: string;
|
|
89
|
+
isSuccess: boolean;
|
|
90
|
+
memoryType?: "project" | "user"; // Memory type
|
|
91
|
+
storagePath?: string; // Storage path text
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface CustomCommandBlock {
|
|
95
|
+
type: "custom_command";
|
|
96
|
+
commandName: string;
|
|
97
|
+
content: string; // Complete command content, used when passing to AI
|
|
98
|
+
originalInput?: string; // Original user input, used for UI display (e.g., "/fix-issue 123 high")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface AIRequest {
|
|
102
|
+
content: string;
|
|
103
|
+
files: unknown[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface AIResponse {
|
|
107
|
+
content: string;
|
|
108
|
+
status: "success" | "error";
|
|
109
|
+
error?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// MCP related types
|
|
113
|
+
export interface McpServerConfig {
|
|
114
|
+
command: string;
|
|
115
|
+
args?: string[];
|
|
116
|
+
env?: Record<string, string>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface McpConfig {
|
|
120
|
+
mcpServers: Record<string, McpServerConfig>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface McpTool {
|
|
124
|
+
name: string;
|
|
125
|
+
description?: string;
|
|
126
|
+
inputSchema: Record<string, unknown>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface McpServerStatus {
|
|
130
|
+
name: string;
|
|
131
|
+
config: McpServerConfig;
|
|
132
|
+
status: "disconnected" | "connected" | "connecting" | "error";
|
|
133
|
+
tools?: McpTool[];
|
|
134
|
+
toolCount?: number;
|
|
135
|
+
capabilities?: string[];
|
|
136
|
+
lastConnected?: number;
|
|
137
|
+
error?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Background bash shell related types
|
|
141
|
+
export interface BackgroundShell {
|
|
142
|
+
id: string;
|
|
143
|
+
process: ChildProcess;
|
|
144
|
+
command: string;
|
|
145
|
+
startTime: number;
|
|
146
|
+
status: "running" | "completed" | "killed";
|
|
147
|
+
stdout: string;
|
|
148
|
+
stderr: string;
|
|
149
|
+
exitCode?: number;
|
|
150
|
+
runtime?: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Slash Command related types
|
|
154
|
+
export interface SlashCommand {
|
|
155
|
+
id: string;
|
|
156
|
+
name: string;
|
|
157
|
+
description: string;
|
|
158
|
+
handler: (args?: string) => Promise<void> | void;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface CustomSlashCommandConfig {
|
|
162
|
+
allowedTools?: string[];
|
|
163
|
+
model?: string;
|
|
164
|
+
description?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface CustomSlashCommand {
|
|
168
|
+
id: string;
|
|
169
|
+
name: string;
|
|
170
|
+
description?: string; // Add description field
|
|
171
|
+
filePath: string;
|
|
172
|
+
content: string;
|
|
173
|
+
config?: CustomSlashCommandConfig;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Skill related types
|
|
177
|
+
export interface SkillMetadata {
|
|
178
|
+
name: string;
|
|
179
|
+
description: string;
|
|
180
|
+
type: "personal" | "project";
|
|
181
|
+
skillPath: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface Skill extends SkillMetadata {
|
|
185
|
+
content: string;
|
|
186
|
+
frontmatter: SkillFrontmatter;
|
|
187
|
+
isValid: boolean;
|
|
188
|
+
errors: string[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface SkillFrontmatter {
|
|
192
|
+
name: string;
|
|
193
|
+
description: string;
|
|
194
|
+
[key: string]: unknown;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface SkillCollection {
|
|
198
|
+
type: "personal" | "project";
|
|
199
|
+
basePath: string;
|
|
200
|
+
skills: Map<string, SkillMetadata>;
|
|
201
|
+
errors: SkillError[];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface SkillError {
|
|
205
|
+
skillPath: string;
|
|
206
|
+
message: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface SkillValidationResult {
|
|
210
|
+
isValid: boolean;
|
|
211
|
+
skill?: Skill;
|
|
212
|
+
errors: string[];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface SkillDiscoveryResult {
|
|
216
|
+
personalSkills: Map<string, SkillMetadata>;
|
|
217
|
+
projectSkills: Map<string, SkillMetadata>;
|
|
218
|
+
errors: SkillError[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface SkillInvocationContext {
|
|
222
|
+
skillName: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface SkillToolArgs {
|
|
226
|
+
skill_name: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface SkillManagerOptions {
|
|
230
|
+
personalSkillsPath?: string;
|
|
231
|
+
scanTimeout?: number;
|
|
232
|
+
logger?: Logger;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface ParsedSkillFile {
|
|
236
|
+
frontmatter: SkillFrontmatter;
|
|
237
|
+
content: string;
|
|
238
|
+
skillMetadata: SkillMetadata;
|
|
239
|
+
validationErrors: string[];
|
|
240
|
+
isValid: boolean;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface SkillParseOptions {
|
|
244
|
+
validateMetadata?: boolean;
|
|
245
|
+
basePath?: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export const SKILL_DEFAULTS = {
|
|
249
|
+
PERSONAL_SKILLS_DIR: ".wave/skills",
|
|
250
|
+
PROJECT_SKILLS_DIR: ".wave/skills",
|
|
251
|
+
SKILL_FILE_NAME: "SKILL.md",
|
|
252
|
+
MAX_NAME_LENGTH: 64,
|
|
253
|
+
MAX_DESCRIPTION_LENGTH: 1024,
|
|
254
|
+
MIN_DESCRIPTION_LENGTH: 1,
|
|
255
|
+
NAME_PATTERN: /^[a-z0-9-]+$/,
|
|
256
|
+
MAX_METADATA_CACHE: 1000,
|
|
257
|
+
MAX_CONTENT_CACHE: 100,
|
|
258
|
+
SCAN_TIMEOUT: 5000,
|
|
259
|
+
LOAD_TIMEOUT: 2000,
|
|
260
|
+
} as const;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash command history management module
|
|
3
|
+
* Used for persistent storage and searching of bash commands executed by users
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import { BASH_HISTORY_FILE, DATA_DIRECTORY } from "./constants.js";
|
|
8
|
+
|
|
9
|
+
export interface BashHistoryEntry {
|
|
10
|
+
command: string;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
workdir: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BashHistory {
|
|
16
|
+
commands: BashHistoryEntry[];
|
|
17
|
+
version: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const HISTORY_VERSION = 1;
|
|
21
|
+
const MAX_HISTORY_SIZE = 1000;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensure data directory exists
|
|
25
|
+
*/
|
|
26
|
+
const ensureDataDirectory = (): void => {
|
|
27
|
+
try {
|
|
28
|
+
if (!fs.existsSync(DATA_DIRECTORY)) {
|
|
29
|
+
fs.mkdirSync(DATA_DIRECTORY, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// logger.debug("Failed to create data directory:", error);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load bash history
|
|
38
|
+
*/
|
|
39
|
+
export const loadBashHistory = (): BashHistory => {
|
|
40
|
+
try {
|
|
41
|
+
ensureDataDirectory();
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(BASH_HISTORY_FILE)) {
|
|
44
|
+
return {
|
|
45
|
+
commands: [],
|
|
46
|
+
version: HISTORY_VERSION,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = fs.readFileSync(BASH_HISTORY_FILE, "utf-8");
|
|
51
|
+
const history: BashHistory = JSON.parse(data);
|
|
52
|
+
|
|
53
|
+
// Version compatibility check
|
|
54
|
+
if (history.version !== HISTORY_VERSION) {
|
|
55
|
+
// logger.debug("Bash history version mismatch, resetting history");
|
|
56
|
+
return {
|
|
57
|
+
commands: [],
|
|
58
|
+
version: HISTORY_VERSION,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return history;
|
|
63
|
+
} catch {
|
|
64
|
+
// logger.debug("Failed to load bash history:", error);
|
|
65
|
+
return {
|
|
66
|
+
commands: [],
|
|
67
|
+
version: HISTORY_VERSION,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Save bash history
|
|
74
|
+
*/
|
|
75
|
+
export const saveBashHistory = (history: BashHistory): void => {
|
|
76
|
+
try {
|
|
77
|
+
// Skip saving to file when in test environment
|
|
78
|
+
if (process.env.NODE_ENV === "test") {
|
|
79
|
+
// logger.debug("Skipping bash history save in test environment");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ensureDataDirectory();
|
|
84
|
+
|
|
85
|
+
// Limit history size
|
|
86
|
+
if (history.commands.length > MAX_HISTORY_SIZE) {
|
|
87
|
+
history.commands = history.commands.slice(-MAX_HISTORY_SIZE);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const data = JSON.stringify(history, null, 2);
|
|
91
|
+
fs.writeFileSync(BASH_HISTORY_FILE, data, "utf-8");
|
|
92
|
+
} catch {
|
|
93
|
+
// logger.debug("Failed to save bash history:", error);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Add command to bash history
|
|
99
|
+
*/
|
|
100
|
+
export const addBashCommandToHistory = (
|
|
101
|
+
command: string,
|
|
102
|
+
workdir: string,
|
|
103
|
+
): void => {
|
|
104
|
+
try {
|
|
105
|
+
// Filter system-generated commands, do not add to history
|
|
106
|
+
if (command.startsWith("git add . && git commit -m")) {
|
|
107
|
+
// logger.debug("Skipping system-generated command:", { command, workdir });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const history = loadBashHistory();
|
|
112
|
+
const timestamp = Date.now();
|
|
113
|
+
|
|
114
|
+
// Check if it's a duplicate consecutive command to avoid duplicate recording
|
|
115
|
+
const lastCommand = history.commands[history.commands.length - 1];
|
|
116
|
+
if (
|
|
117
|
+
lastCommand &&
|
|
118
|
+
lastCommand.command === command &&
|
|
119
|
+
lastCommand.workdir === workdir
|
|
120
|
+
) {
|
|
121
|
+
// Update timestamp of the last record
|
|
122
|
+
lastCommand.timestamp = timestamp;
|
|
123
|
+
} else {
|
|
124
|
+
// Add new command record
|
|
125
|
+
history.commands.push({
|
|
126
|
+
command,
|
|
127
|
+
timestamp,
|
|
128
|
+
workdir,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
saveBashHistory(history);
|
|
133
|
+
// logger.debug("Added bash command to history:", { command, workdir });
|
|
134
|
+
} catch {
|
|
135
|
+
// logger.debug("Failed to add bash command to history:", error);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Search bash history by keywords
|
|
141
|
+
*/
|
|
142
|
+
export const searchBashHistory = (
|
|
143
|
+
query: string,
|
|
144
|
+
limit: number = 10,
|
|
145
|
+
workdir: string,
|
|
146
|
+
): BashHistoryEntry[] => {
|
|
147
|
+
try {
|
|
148
|
+
const history = loadBashHistory();
|
|
149
|
+
const normalizedQuery = query.toLowerCase().trim();
|
|
150
|
+
|
|
151
|
+
let filteredCommands = history.commands;
|
|
152
|
+
|
|
153
|
+
// Working directory filter
|
|
154
|
+
filteredCommands = filteredCommands.filter(
|
|
155
|
+
(entry) => entry.workdir === workdir,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (!normalizedQuery) {
|
|
159
|
+
// If no search query, return recent commands (deduplicated)
|
|
160
|
+
const deduped = deduplicateCommands(filteredCommands);
|
|
161
|
+
return deduped.slice(-limit).reverse(); // Latest first
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Search by relevance
|
|
165
|
+
const matches = filteredCommands
|
|
166
|
+
.filter((entry) => {
|
|
167
|
+
// Command content matching
|
|
168
|
+
const command = entry.command.toLowerCase();
|
|
169
|
+
return command.includes(normalizedQuery);
|
|
170
|
+
})
|
|
171
|
+
.map((entry) => {
|
|
172
|
+
// Calculate match score
|
|
173
|
+
const command = entry.command.toLowerCase();
|
|
174
|
+
let score = 0;
|
|
175
|
+
|
|
176
|
+
// Exact match gets higher score
|
|
177
|
+
if (command.includes(normalizedQuery)) {
|
|
178
|
+
score += 10;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Command prefix match gets higher score
|
|
182
|
+
if (command.startsWith(normalizedQuery)) {
|
|
183
|
+
score += 20;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Word boundary match gets higher score
|
|
187
|
+
const words = command.split(/\s+/);
|
|
188
|
+
if (words.some((word) => word.startsWith(normalizedQuery))) {
|
|
189
|
+
score += 15;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Timestamp influence (newer gets higher score)
|
|
193
|
+
score += entry.timestamp / 1000000; // Normalize timestamp
|
|
194
|
+
|
|
195
|
+
return { entry, score };
|
|
196
|
+
})
|
|
197
|
+
.sort((a, b) => b.score - a.score) // Sort by score descending
|
|
198
|
+
.map((item) => item.entry);
|
|
199
|
+
|
|
200
|
+
// Deduplicate search results, keep latest record
|
|
201
|
+
const dedupedMatches = deduplicateCommands(matches);
|
|
202
|
+
const result = dedupedMatches.slice(0, limit);
|
|
203
|
+
|
|
204
|
+
// logger.debug("Bash history search results:", { query, workdir: process.cwd(), originalCount: matches.length, dedupedCount: result.length });
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
} catch {
|
|
208
|
+
// logger.debug("Failed to search bash history:", error);
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Deduplicate command list, keep latest record for each command
|
|
215
|
+
*/
|
|
216
|
+
const deduplicateCommands = (
|
|
217
|
+
commands: BashHistoryEntry[],
|
|
218
|
+
): BashHistoryEntry[] => {
|
|
219
|
+
const commandMap = new Map<string, BashHistoryEntry>();
|
|
220
|
+
|
|
221
|
+
// Iterate through all commands, keep latest record for each
|
|
222
|
+
for (const entry of commands) {
|
|
223
|
+
const existing = commandMap.get(entry.command);
|
|
224
|
+
if (!existing || entry.timestamp > existing.timestamp) {
|
|
225
|
+
commandMap.set(entry.command, entry);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Sort by timestamp and return
|
|
230
|
+
return Array.from(commandMap.values()).sort(
|
|
231
|
+
(a, b) => a.timestamp - b.timestamp,
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get recently used bash commands
|
|
237
|
+
*/
|
|
238
|
+
export const getRecentBashCommands = (
|
|
239
|
+
workdir: string,
|
|
240
|
+
limit: number = 10,
|
|
241
|
+
): BashHistoryEntry[] => {
|
|
242
|
+
try {
|
|
243
|
+
const history = loadBashHistory();
|
|
244
|
+
const filtered = history.commands.filter(
|
|
245
|
+
(entry) => entry.workdir === workdir,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Return recent commands after deduplication
|
|
249
|
+
const deduped = deduplicateCommands(filtered);
|
|
250
|
+
return deduped.slice(-limit).reverse(); // Latest first
|
|
251
|
+
} catch {
|
|
252
|
+
// logger.debug("Failed to get recent bash commands:", error);
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Clear bash history
|
|
259
|
+
*/
|
|
260
|
+
export const clearBashHistory = (): void => {
|
|
261
|
+
try {
|
|
262
|
+
const history: BashHistory = {
|
|
263
|
+
commands: [],
|
|
264
|
+
version: HISTORY_VERSION,
|
|
265
|
+
};
|
|
266
|
+
saveBashHistory(history);
|
|
267
|
+
// logger.debug("Bash history cleared");
|
|
268
|
+
} catch {
|
|
269
|
+
// logger.debug("Failed to clear bash history:", error);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get bash command statistics
|
|
275
|
+
*/
|
|
276
|
+
export const getBashCommandStats = (): {
|
|
277
|
+
totalCommands: number;
|
|
278
|
+
uniqueCommands: number;
|
|
279
|
+
workdirs: string[];
|
|
280
|
+
} => {
|
|
281
|
+
try {
|
|
282
|
+
const history = loadBashHistory();
|
|
283
|
+
const uniqueCommands = new Set(
|
|
284
|
+
history.commands.map((entry) => entry.command),
|
|
285
|
+
);
|
|
286
|
+
const workdirs = Array.from(
|
|
287
|
+
new Set(history.commands.map((entry) => entry.workdir)),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
totalCommands: history.commands.length,
|
|
292
|
+
uniqueCommands: uniqueCommands.size,
|
|
293
|
+
workdirs,
|
|
294
|
+
};
|
|
295
|
+
} catch {
|
|
296
|
+
// logger.debug("Failed to get bash command stats:", error);
|
|
297
|
+
return {
|
|
298
|
+
totalCommands: 0,
|
|
299
|
+
uniqueCommands: 0,
|
|
300
|
+
workdirs: [],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Argument Parser
|
|
3
|
+
*
|
|
4
|
+
* Provides parameter substitution for custom slash commands similar to Claude's system:
|
|
5
|
+
* - $ARGUMENTS: All arguments as a single string
|
|
6
|
+
* - $1, $2, $3, etc.: Individual positional arguments
|
|
7
|
+
* - Supports quoted arguments with spaces
|
|
8
|
+
* - Handles escaped quotes within arguments
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse command arguments from a string, respecting quotes
|
|
13
|
+
*/
|
|
14
|
+
export function parseCommandArguments(argsString: string): string[] {
|
|
15
|
+
if (!argsString.trim()) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const args: string[] = [];
|
|
20
|
+
let current = "";
|
|
21
|
+
let inQuotes = false;
|
|
22
|
+
let quoteChar = "";
|
|
23
|
+
let escaped = false;
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < argsString.length; i++) {
|
|
26
|
+
const char = argsString[i];
|
|
27
|
+
const nextChar = argsString[i + 1];
|
|
28
|
+
|
|
29
|
+
if (escaped) {
|
|
30
|
+
current += char;
|
|
31
|
+
escaped = false;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (char === "\\") {
|
|
36
|
+
// Handle escape sequences
|
|
37
|
+
if (inQuotes && (nextChar === quoteChar || nextChar === "\\")) {
|
|
38
|
+
escaped = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
current += char;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!inQuotes && (char === '"' || char === "'")) {
|
|
46
|
+
// Start quoted string
|
|
47
|
+
inQuotes = true;
|
|
48
|
+
quoteChar = char;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (inQuotes && char === quoteChar) {
|
|
53
|
+
// End quoted string
|
|
54
|
+
inQuotes = false;
|
|
55
|
+
quoteChar = "";
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!inQuotes && /\s/.test(char)) {
|
|
60
|
+
// Whitespace outside quotes - end current argument
|
|
61
|
+
if (current) {
|
|
62
|
+
args.push(current);
|
|
63
|
+
current = "";
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
current += char;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Add final argument if any
|
|
72
|
+
if (current) {
|
|
73
|
+
args.push(current);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return args;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Substitute command parameters in content
|
|
81
|
+
*/
|
|
82
|
+
export function substituteCommandParameters(
|
|
83
|
+
content: string,
|
|
84
|
+
argsString: string,
|
|
85
|
+
): string {
|
|
86
|
+
const args = parseCommandArguments(argsString);
|
|
87
|
+
|
|
88
|
+
let result = content;
|
|
89
|
+
|
|
90
|
+
// Replace $ARGUMENTS with all arguments
|
|
91
|
+
result = result.replace(/\$ARGUMENTS/g, argsString);
|
|
92
|
+
|
|
93
|
+
// Replace positional parameters $1, $2, etc.
|
|
94
|
+
// Sort by parameter number (descending) to avoid replacing $10 with $1 + "0"
|
|
95
|
+
const positionalParams = [...result.matchAll(/\$(\d+)/g)]
|
|
96
|
+
.map((match) => parseInt(match[1], 10))
|
|
97
|
+
.filter((value, index, array) => array.indexOf(value) === index) // unique
|
|
98
|
+
.sort((a, b) => b - a); // descending order
|
|
99
|
+
|
|
100
|
+
for (const paramNum of positionalParams) {
|
|
101
|
+
const paramValue = args[paramNum - 1] || ""; // Arrays are 0-indexed, params are 1-indexed
|
|
102
|
+
const paramRegex = new RegExp(`\\$${paramNum}`, "g");
|
|
103
|
+
result = result.replace(paramRegex, paramValue);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extract command name and arguments from a slash command input
|
|
111
|
+
* Example: "/fix-issue 123 high-priority" -> { command: "fix-issue", args: "123 high-priority" }
|
|
112
|
+
*/
|
|
113
|
+
export function parseSlashCommandInput(input: string): {
|
|
114
|
+
command: string;
|
|
115
|
+
args: string;
|
|
116
|
+
} {
|
|
117
|
+
const trimmed = input.trim();
|
|
118
|
+
|
|
119
|
+
if (!trimmed.startsWith("/")) {
|
|
120
|
+
throw new Error("Input must start with /");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const withoutSlash = trimmed.substring(1);
|
|
124
|
+
const firstSpaceIndex = withoutSlash.indexOf(" ");
|
|
125
|
+
|
|
126
|
+
if (firstSpaceIndex === -1) {
|
|
127
|
+
// No arguments
|
|
128
|
+
return {
|
|
129
|
+
command: withoutSlash,
|
|
130
|
+
args: "",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
command: withoutSlash.substring(0, firstSpaceIndex),
|
|
136
|
+
args: withoutSlash.substring(firstSpaceIndex + 1).trim(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if content contains parameter placeholders
|
|
142
|
+
*/
|
|
143
|
+
export function hasParameterPlaceholders(content: string): boolean {
|
|
144
|
+
return /\$(?:ARGUMENTS|\d+)/.test(content);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get all parameter placeholders used in content
|
|
149
|
+
*/
|
|
150
|
+
export function getUsedParameterPlaceholders(content: string): string[] {
|
|
151
|
+
const matches = content.match(/\$(?:ARGUMENTS|\d+)/g);
|
|
152
|
+
return matches ? [...new Set(matches)] : [];
|
|
153
|
+
}
|