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
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
+ }