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,265 @@
1
+ import { promises as fs } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import type { Message } from "../types.js";
5
+
6
+ export interface SessionData {
7
+ id: string;
8
+ timestamp: string;
9
+ version: string;
10
+ messages: Message[];
11
+ metadata: {
12
+ workdir: string;
13
+ startedAt: string;
14
+ lastActiveAt: string;
15
+ latestTotalTokens: number;
16
+ };
17
+ }
18
+
19
+ export interface SessionMetadata {
20
+ id: string;
21
+ timestamp: string;
22
+ workdir: string;
23
+ startedAt: string;
24
+ lastActiveAt: string;
25
+ latestTotalTokens: number;
26
+ }
27
+
28
+ // Constants
29
+ const SESSION_DIR = join(homedir(), ".wave", "sessions");
30
+ const VERSION = "1.0.0";
31
+ const MAX_SESSION_AGE_DAYS = 30;
32
+
33
+ /**
34
+ * Ensure session directory exists
35
+ */
36
+ async function ensureSessionDir(): Promise<void> {
37
+ try {
38
+ await fs.mkdir(SESSION_DIR, { recursive: true });
39
+ } catch (error) {
40
+ throw new Error(`Failed to create session directory: ${error}`);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Generate session file path
46
+ */
47
+ export function getSessionFilePath(sessionId: string): string {
48
+ const shortId = sessionId.split("_")[2] || sessionId.slice(-8);
49
+ return join(SESSION_DIR, `session_${shortId}.json`);
50
+ }
51
+
52
+ /**
53
+ * Filter out diff blocks from messages to avoid saving unimportant data
54
+ */
55
+ function filterDiffBlocks(messages: Message[]): Message[] {
56
+ return messages
57
+ .map((message) => ({
58
+ ...message,
59
+ blocks: message.blocks.filter((block) => block.type !== "diff"),
60
+ }))
61
+ .filter((message) => message.blocks.length > 0);
62
+ }
63
+
64
+ /**
65
+ * Save session data
66
+ */
67
+ export async function saveSession(
68
+ sessionId: string,
69
+ messages: Message[],
70
+ workdir: string,
71
+ latestTotalTokens: number = 0,
72
+ startedAt?: string,
73
+ ): Promise<void> {
74
+ // Do not save session files in test environment
75
+ if (process.env.NODE_ENV === "test") {
76
+ return;
77
+ }
78
+
79
+ await ensureSessionDir();
80
+
81
+ // Filter out diff blocks before saving
82
+ const filteredMessages = filterDiffBlocks(messages);
83
+
84
+ const now = new Date().toISOString();
85
+ const sessionData: SessionData = {
86
+ id: sessionId,
87
+ timestamp: now,
88
+ version: VERSION,
89
+ messages: filteredMessages,
90
+ metadata: {
91
+ workdir: workdir,
92
+ startedAt: startedAt || now,
93
+ lastActiveAt: now,
94
+ latestTotalTokens,
95
+ },
96
+ };
97
+
98
+ const filePath = getSessionFilePath(sessionId);
99
+ try {
100
+ await fs.writeFile(filePath, JSON.stringify(sessionData, null, 2), "utf-8");
101
+ } catch (error) {
102
+ throw new Error(`Failed to save session ${sessionId}: ${error}`);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Load session data
108
+ */
109
+ export async function loadSession(
110
+ sessionId: string,
111
+ ): Promise<SessionData | null> {
112
+ const filePath = getSessionFilePath(sessionId);
113
+
114
+ try {
115
+ const content = await fs.readFile(filePath, "utf-8");
116
+ const sessionData = JSON.parse(content) as SessionData;
117
+
118
+ // Validate session data format
119
+ if (!sessionData.id || !sessionData.messages || !sessionData.metadata) {
120
+ throw new Error("Invalid session data format");
121
+ }
122
+
123
+ return sessionData;
124
+ } catch (error) {
125
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
126
+ return null; // Session file does not exist
127
+ }
128
+ throw new Error(`Failed to load session ${sessionId}: ${error}`);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Get most recent session
134
+ */
135
+ export async function getLatestSession(
136
+ workdir: string,
137
+ ): Promise<SessionData | null> {
138
+ const sessions = await listSessions(workdir);
139
+ if (sessions.length === 0) {
140
+ return null;
141
+ }
142
+
143
+ // Sort by last active time, return the latest
144
+ const latestSession = sessions.sort(
145
+ (a, b) =>
146
+ new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime(),
147
+ )[0];
148
+
149
+ return loadSession(latestSession.id);
150
+ }
151
+
152
+ /**
153
+ * List all sessions
154
+ */
155
+ export async function listSessions(
156
+ workdir: string,
157
+ includeAllWorkdirs = false,
158
+ ): Promise<SessionMetadata[]> {
159
+ try {
160
+ await ensureSessionDir();
161
+ const files = await fs.readdir(SESSION_DIR);
162
+
163
+ const sessions: SessionMetadata[] = [];
164
+
165
+ for (const file of files) {
166
+ if (!file.startsWith("session_") || !file.endsWith(".json")) {
167
+ continue;
168
+ }
169
+
170
+ try {
171
+ const filePath = join(SESSION_DIR, file);
172
+ const content = await fs.readFile(filePath, "utf-8");
173
+ const sessionData = JSON.parse(content) as SessionData;
174
+
175
+ // Only return sessions for the current working directory, unless includeAllWorkdirs is true
176
+ if (!includeAllWorkdirs && sessionData.metadata.workdir !== workdir) {
177
+ continue;
178
+ }
179
+
180
+ sessions.push({
181
+ id: sessionData.id,
182
+ timestamp: sessionData.timestamp,
183
+ workdir: sessionData.metadata.workdir,
184
+ startedAt: sessionData.metadata.startedAt,
185
+ lastActiveAt: sessionData.metadata.lastActiveAt,
186
+ latestTotalTokens: sessionData.metadata.latestTotalTokens,
187
+ });
188
+ } catch {
189
+ // Ignore corrupted session files
190
+ console.warn(`Skipping corrupted session file: ${file}`);
191
+ }
192
+ }
193
+
194
+ return sessions.sort(
195
+ (a, b) =>
196
+ new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime(),
197
+ );
198
+ } catch (error) {
199
+ throw new Error(`Failed to list sessions: ${error}`);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Delete session
205
+ */
206
+ export async function deleteSession(sessionId: string): Promise<boolean> {
207
+ const filePath = getSessionFilePath(sessionId);
208
+
209
+ try {
210
+ await fs.unlink(filePath);
211
+ return true;
212
+ } catch (error) {
213
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
214
+ return false; // File does not exist
215
+ }
216
+ throw new Error(`Failed to delete session ${sessionId}: ${error}`);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Clean up expired sessions
222
+ */
223
+ export async function cleanupExpiredSessions(workdir: string): Promise<number> {
224
+ // Do not perform cleanup operations in test environment
225
+ if (process.env.NODE_ENV === "test") {
226
+ return 0;
227
+ }
228
+
229
+ const sessions = await listSessions(workdir, true);
230
+ const now = new Date();
231
+ const maxAge = MAX_SESSION_AGE_DAYS * 24 * 60 * 60 * 1000; // Convert to milliseconds
232
+
233
+ let deletedCount = 0;
234
+
235
+ for (const session of sessions) {
236
+ const sessionAge = now.getTime() - new Date(session.lastActiveAt).getTime();
237
+
238
+ if (sessionAge > maxAge) {
239
+ try {
240
+ await deleteSession(session.id);
241
+ deletedCount++;
242
+ } catch (error) {
243
+ console.warn(
244
+ `Failed to delete expired session ${session.id}: ${error}`,
245
+ );
246
+ }
247
+ }
248
+ }
249
+
250
+ return deletedCount;
251
+ }
252
+
253
+ /**
254
+ * Check if session exists
255
+ */
256
+ export async function sessionExists(sessionId: string): Promise<boolean> {
257
+ const filePath = getSessionFilePath(sessionId);
258
+
259
+ try {
260
+ await fs.access(filePath);
261
+ return true;
262
+ } catch {
263
+ return false;
264
+ }
265
+ }
@@ -0,0 +1,402 @@
1
+ import { spawn, ChildProcess } from "child_process";
2
+ import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
3
+
4
+ /**
5
+ * Bash command execution tool - supports both foreground and background execution
6
+ */
7
+ export const bashTool: ToolPlugin = {
8
+ name: "Bash",
9
+ config: {
10
+ type: "function",
11
+ function: {
12
+ name: "Bash",
13
+ description:
14
+ "Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.",
15
+ parameters: {
16
+ type: "object",
17
+ properties: {
18
+ command: {
19
+ type: "string",
20
+ description: "The command to execute",
21
+ },
22
+ timeout: {
23
+ type: "number",
24
+ description: "Optional timeout in milliseconds (max 600000)",
25
+ },
26
+ description: {
27
+ type: "string",
28
+ description:
29
+ "Clear, concise description of what this command does in 5-10 words.",
30
+ },
31
+ run_in_background: {
32
+ type: "boolean",
33
+ description:
34
+ "Set to true to run this command in the background. Use BashOutput to read the output later.",
35
+ },
36
+ },
37
+ required: ["command"],
38
+ },
39
+ },
40
+ },
41
+ execute: async (
42
+ args: Record<string, unknown>,
43
+ context: ToolContext,
44
+ ): Promise<ToolResult> => {
45
+ const command = args.command as string;
46
+ const runInBackground = args.run_in_background as boolean | undefined;
47
+ // Set default timeout: 30s for foreground, no timeout for background
48
+ const timeout =
49
+ (args.timeout as number | undefined) ??
50
+ (runInBackground ? undefined : 30000);
51
+
52
+ if (!command || typeof command !== "string") {
53
+ return {
54
+ success: false,
55
+ content: "",
56
+ error: "Command parameter is required and must be a string",
57
+ };
58
+ }
59
+
60
+ // Validate timeout
61
+ if (
62
+ timeout !== undefined &&
63
+ (typeof timeout !== "number" || timeout < 0 || timeout > 600000)
64
+ ) {
65
+ return {
66
+ success: false,
67
+ content: "",
68
+ error: "Timeout must be a number between 0 and 600000 milliseconds",
69
+ };
70
+ }
71
+
72
+ if (runInBackground) {
73
+ // Background execution
74
+ const backgroundBashManager = context?.backgroundBashManager;
75
+ if (!backgroundBashManager) {
76
+ return {
77
+ success: false,
78
+ content: "",
79
+ error: "Background bash manager not available",
80
+ };
81
+ }
82
+
83
+ const shellId = backgroundBashManager.startShell(command, timeout);
84
+ return {
85
+ success: true,
86
+ content: `Command started in background with ID: ${shellId}. Use BashOutput tool with bash_id="${shellId}" to monitor output.`,
87
+ shortResult: `Background process ${shellId} started`,
88
+ };
89
+ }
90
+
91
+ // Foreground execution (original behavior)
92
+ return new Promise((resolve) => {
93
+ const child: ChildProcess = spawn(command, {
94
+ shell: true,
95
+ stdio: "pipe",
96
+ cwd: context.workdir,
97
+ env: {
98
+ ...process.env,
99
+ },
100
+ });
101
+
102
+ let outputBuffer = "";
103
+ let errorBuffer = "";
104
+ let isAborted = false;
105
+
106
+ // Set up timeout
107
+ let timeoutHandle: NodeJS.Timeout | undefined;
108
+ if (timeout && timeout > 0) {
109
+ timeoutHandle = setTimeout(() => {
110
+ if (!isAborted) {
111
+ handleAbort("Command timed out");
112
+ }
113
+ }, timeout);
114
+ }
115
+
116
+ // Handle abort signal
117
+ const handleAbort = (reason = "Command execution was aborted") => {
118
+ if (!isAborted) {
119
+ isAborted = true;
120
+
121
+ if (timeoutHandle) {
122
+ clearTimeout(timeoutHandle);
123
+ }
124
+
125
+ // Force terminate child process and its children
126
+ if (child.pid) {
127
+ try {
128
+ // Try graceful termination of process group
129
+ process.kill(-child.pid, "SIGTERM");
130
+
131
+ // Set timeout for force kill
132
+ setTimeout(() => {
133
+ if (child.pid && !child.killed) {
134
+ try {
135
+ process.kill(-child.pid, "SIGKILL");
136
+ } catch {
137
+ // logger.error("Failed to force kill process:", killError);
138
+ }
139
+ }
140
+ }, 1000);
141
+ } catch {
142
+ // If process group termination fails, try direct child process termination
143
+ try {
144
+ child.kill("SIGTERM");
145
+ setTimeout(() => {
146
+ if (!child.killed) {
147
+ child.kill("SIGKILL");
148
+ }
149
+ }, 1000);
150
+ } catch {
151
+ // logger.error("Failed to kill child process:", directKillError);
152
+ }
153
+ }
154
+ }
155
+
156
+ resolve({
157
+ success: false,
158
+ content: outputBuffer + (errorBuffer ? "\n" + errorBuffer : ""),
159
+ error: reason,
160
+ });
161
+ }
162
+ };
163
+
164
+ // Handle abort signal from context
165
+ if (context?.abortSignal) {
166
+ if (context.abortSignal.aborted) {
167
+ handleAbort();
168
+ return;
169
+ }
170
+ context.abortSignal.addEventListener("abort", () => handleAbort());
171
+ }
172
+
173
+ child.stdout?.on("data", (data) => {
174
+ if (!isAborted) {
175
+ outputBuffer += data.toString();
176
+ }
177
+ });
178
+
179
+ child.stderr?.on("data", (data) => {
180
+ if (!isAborted) {
181
+ errorBuffer += data.toString();
182
+ }
183
+ });
184
+
185
+ child.on("exit", (code) => {
186
+ if (!isAborted) {
187
+ if (timeoutHandle) {
188
+ clearTimeout(timeoutHandle);
189
+ }
190
+
191
+ const exitCode = code ?? 0;
192
+ const combinedOutput =
193
+ outputBuffer + (errorBuffer ? "\n" + errorBuffer : "");
194
+
195
+ resolve({
196
+ success: exitCode === 0,
197
+ content:
198
+ combinedOutput || `Command executed with exit code: ${exitCode}`,
199
+ error:
200
+ exitCode !== 0
201
+ ? `Command failed with exit code: ${exitCode}`
202
+ : undefined,
203
+ });
204
+ }
205
+ });
206
+
207
+ child.on("error", (error) => {
208
+ if (!isAborted) {
209
+ if (timeoutHandle) {
210
+ clearTimeout(timeoutHandle);
211
+ }
212
+ resolve({
213
+ success: false,
214
+ content: "",
215
+ error: `Failed to execute command: ${error.message}`,
216
+ });
217
+ }
218
+ });
219
+ });
220
+ },
221
+ formatCompactParams: (params: Record<string, unknown>) => {
222
+ const command = params.command as string;
223
+ const runInBackground = params.run_in_background as boolean;
224
+ return `${command}${runInBackground ? " background" : ""}`;
225
+ },
226
+ };
227
+
228
+ /**
229
+ * BashOutput tool - retrieves output from background bash shells
230
+ */
231
+ export const bashOutputTool: ToolPlugin = {
232
+ name: "BashOutput",
233
+ config: {
234
+ type: "function",
235
+ function: {
236
+ name: "BashOutput",
237
+ description:
238
+ "Retrieves output from a running or completed background bash shell",
239
+ parameters: {
240
+ type: "object",
241
+ properties: {
242
+ bash_id: {
243
+ type: "string",
244
+ description:
245
+ "The ID of the background shell to retrieve output from",
246
+ },
247
+ filter: {
248
+ type: "string",
249
+ description:
250
+ "Optional regular expression to filter the output lines. Only lines matching this regex will be included in the result. Any lines that do not match will no longer be available to read.",
251
+ },
252
+ },
253
+ required: ["bash_id"],
254
+ },
255
+ },
256
+ },
257
+ execute: async (
258
+ args: Record<string, unknown>,
259
+ context: ToolContext,
260
+ ): Promise<ToolResult> => {
261
+ const bashId = args.bash_id as string;
262
+ const filter = args.filter as string | undefined;
263
+
264
+ if (!bashId || typeof bashId !== "string") {
265
+ return {
266
+ success: false,
267
+ content: "",
268
+ error: "bash_id parameter is required and must be a string",
269
+ };
270
+ }
271
+
272
+ const backgroundBashManager = context?.backgroundBashManager;
273
+ if (!backgroundBashManager) {
274
+ return {
275
+ success: false,
276
+ content: "",
277
+ error: "Background bash manager not available",
278
+ };
279
+ }
280
+
281
+ const output = backgroundBashManager.getOutput(bashId, filter);
282
+ if (!output) {
283
+ return {
284
+ success: false,
285
+ content: "",
286
+ error: `Background shell with ID ${bashId} not found`,
287
+ };
288
+ }
289
+
290
+ const shell = backgroundBashManager.getShell(bashId);
291
+ if (!shell) {
292
+ return {
293
+ success: false,
294
+ content: "",
295
+ error: `Background shell with ID ${bashId} not found`,
296
+ };
297
+ }
298
+
299
+ let content = "";
300
+ if (output.stdout) {
301
+ content += output.stdout;
302
+ }
303
+ if (output.stderr) {
304
+ content += (content ? "\n" : "") + output.stderr;
305
+ }
306
+
307
+ return {
308
+ success: true,
309
+ content: content || "No output available",
310
+ shortResult: `${bashId}: ${output.status}${shell.exitCode !== undefined ? ` (${shell.exitCode})` : ""}`,
311
+ error: undefined,
312
+ };
313
+ },
314
+ formatCompactParams: (params: Record<string, unknown>) => {
315
+ const bashId = params.bash_id as string;
316
+ const filter = params.filter as string | undefined;
317
+ return filter ? `${bashId} filtered: ${filter}` : bashId;
318
+ },
319
+ };
320
+
321
+ /**
322
+ * KillBash tool - kills a running background bash shell
323
+ */
324
+ export const killBashTool: ToolPlugin = {
325
+ name: "KillBash",
326
+ config: {
327
+ type: "function",
328
+ function: {
329
+ name: "KillBash",
330
+ description: "Kills a running background bash shell by its ID",
331
+ parameters: {
332
+ type: "object",
333
+ properties: {
334
+ shell_id: {
335
+ type: "string",
336
+ description: "The ID of the background shell to kill",
337
+ },
338
+ },
339
+ required: ["shell_id"],
340
+ },
341
+ },
342
+ },
343
+ execute: async (
344
+ args: Record<string, unknown>,
345
+ context: ToolContext,
346
+ ): Promise<ToolResult> => {
347
+ const shellId = args.shell_id as string;
348
+
349
+ if (!shellId || typeof shellId !== "string") {
350
+ return {
351
+ success: false,
352
+ content: "",
353
+ error: "shell_id parameter is required and must be a string",
354
+ };
355
+ }
356
+
357
+ const backgroundBashManager = context?.backgroundBashManager;
358
+ if (!backgroundBashManager) {
359
+ return {
360
+ success: false,
361
+ content: "",
362
+ error: "Background bash manager not available",
363
+ };
364
+ }
365
+
366
+ const shell = backgroundBashManager.getShell(shellId);
367
+ if (!shell) {
368
+ return {
369
+ success: false,
370
+ content: "",
371
+ error: `Background shell with ID ${shellId} not found`,
372
+ };
373
+ }
374
+
375
+ if (shell.status !== "running") {
376
+ return {
377
+ success: false,
378
+ content: "",
379
+ error: `Background shell ${shellId} is not running (status: ${shell.status})`,
380
+ };
381
+ }
382
+
383
+ const killed = backgroundBashManager.killShell(shellId);
384
+ if (killed) {
385
+ return {
386
+ success: true,
387
+ content: `Background shell ${shellId} has been killed`,
388
+ shortResult: `Killed ${shellId}`,
389
+ };
390
+ } else {
391
+ return {
392
+ success: false,
393
+ content: "",
394
+ error: `Failed to kill background shell ${shellId}`,
395
+ };
396
+ }
397
+ },
398
+ formatCompactParams: (params: Record<string, unknown>) => {
399
+ const shellId = params.shell_id as string;
400
+ return shellId;
401
+ },
402
+ };