seahorse-bash-client 1.0.0
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/DESIGN.md +463 -0
- package/bin/cli.js +2 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +190 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/pty-manager.d.ts +52 -0
- package/dist/pty-manager.d.ts.map +1 -0
- package/dist/pty-manager.js +387 -0
- package/dist/pty-manager.js.map +1 -0
- package/dist/pty-manager.test.d.ts +8 -0
- package/dist/pty-manager.test.d.ts.map +1 -0
- package/dist/pty-manager.test.js +427 -0
- package/dist/pty-manager.test.js.map +1 -0
- package/dist/tools.d.ts +6 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +212 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +190 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/websocket-client.d.ts +73 -0
- package/dist/websocket-client.d.ts.map +1 -0
- package/dist/websocket-client.js +318 -0
- package/dist/websocket-client.js.map +1 -0
- package/package.json +55 -0
- package/src/cli.ts +179 -0
- package/src/index.ts +10 -0
- package/src/pty-manager.test.ts +555 -0
- package/src/pty-manager.ts +430 -0
- package/src/tools.ts +217 -0
- package/src/types.ts +221 -0
- package/src/websocket-client.ts +374 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY Manager - Manages multiple PTY sessions with rolling output buffers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as pty from 'node-pty';
|
|
6
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import {
|
|
9
|
+
PTYSession,
|
|
10
|
+
SessionStatus,
|
|
11
|
+
OutputLine,
|
|
12
|
+
BashSpawnInput,
|
|
13
|
+
BashExecInput,
|
|
14
|
+
BashWriteInput,
|
|
15
|
+
BashReadInput,
|
|
16
|
+
BashListInput,
|
|
17
|
+
BashKillInput,
|
|
18
|
+
BashSpawnResponse,
|
|
19
|
+
BashExecResponse,
|
|
20
|
+
BashWriteResponse,
|
|
21
|
+
BashReadResponse,
|
|
22
|
+
BashListResponse,
|
|
23
|
+
BashKillResponse,
|
|
24
|
+
} from './types';
|
|
25
|
+
|
|
26
|
+
// Special key mappings
|
|
27
|
+
const SPECIAL_KEYS: Record<string, string> = {
|
|
28
|
+
'ctrl+c': '\x03',
|
|
29
|
+
'ctrl+d': '\x04',
|
|
30
|
+
'ctrl+z': '\x1a',
|
|
31
|
+
'enter': '\r',
|
|
32
|
+
'tab': '\t',
|
|
33
|
+
'up': '\x1b[A',
|
|
34
|
+
'down': '\x1b[B',
|
|
35
|
+
'left': '\x1b[D',
|
|
36
|
+
'right': '\x1b[C',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface ManagedSession {
|
|
40
|
+
session: PTYSession;
|
|
41
|
+
ptyProcess: pty.IPty;
|
|
42
|
+
outputBuffer: OutputLine[];
|
|
43
|
+
notifyOnExit: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class PTYManager extends EventEmitter {
|
|
47
|
+
private sessions: Map<string, ManagedSession> = new Map();
|
|
48
|
+
private maxOutputLines: number;
|
|
49
|
+
private defaultShell: string;
|
|
50
|
+
|
|
51
|
+
constructor(options: { maxOutputLines?: number; defaultShell?: string } = {}) {
|
|
52
|
+
super();
|
|
53
|
+
this.maxOutputLines = options.maxOutputLines || 50000;
|
|
54
|
+
this.defaultShell = options.defaultShell || process.env.SHELL || '/bin/bash';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Spawn a new PTY session
|
|
59
|
+
*/
|
|
60
|
+
async spawn(input: BashSpawnInput): Promise<BashSpawnResponse> {
|
|
61
|
+
const sessionId = `pty_${uuidv4().slice(0, 8)}`;
|
|
62
|
+
const shell = input.shell || this.defaultShell;
|
|
63
|
+
const cwd = input.cwd || process.cwd();
|
|
64
|
+
|
|
65
|
+
// Build the command
|
|
66
|
+
const shellArgs = input.command
|
|
67
|
+
? ['-c', `${input.command}${input.args ? ' ' + input.args.join(' ') : ''}`]
|
|
68
|
+
: [];
|
|
69
|
+
|
|
70
|
+
// Merge environment
|
|
71
|
+
const env = { ...process.env, ...input.env } as Record<string, string>;
|
|
72
|
+
|
|
73
|
+
// Create PTY process
|
|
74
|
+
const ptyProcess = pty.spawn(shell, shellArgs, {
|
|
75
|
+
name: 'xterm-256color',
|
|
76
|
+
cols: input.cols || 120,
|
|
77
|
+
rows: input.rows || 30,
|
|
78
|
+
cwd,
|
|
79
|
+
env,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const session: PTYSession = {
|
|
83
|
+
id: sessionId,
|
|
84
|
+
name: input.name,
|
|
85
|
+
pid: ptyProcess.pid,
|
|
86
|
+
command: input.command || '',
|
|
87
|
+
args: input.args || [],
|
|
88
|
+
cwd,
|
|
89
|
+
status: 'running',
|
|
90
|
+
createdAt: new Date(),
|
|
91
|
+
lineCount: 0,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const managedSession: ManagedSession = {
|
|
95
|
+
session,
|
|
96
|
+
ptyProcess,
|
|
97
|
+
outputBuffer: [],
|
|
98
|
+
notifyOnExit: input.notifyOnExit !== false,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
this.sessions.set(sessionId, managedSession);
|
|
102
|
+
|
|
103
|
+
// Handle output
|
|
104
|
+
ptyProcess.onData((data: string) => {
|
|
105
|
+
this.appendOutput(sessionId, data);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Handle exit
|
|
109
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
110
|
+
const ms = this.sessions.get(sessionId);
|
|
111
|
+
if (ms) {
|
|
112
|
+
ms.session.status = signal ? 'killed' : 'exited';
|
|
113
|
+
ms.session.exitCode = exitCode;
|
|
114
|
+
ms.session.signal = signal?.toString();
|
|
115
|
+
|
|
116
|
+
if (ms.notifyOnExit) {
|
|
117
|
+
this.emit('session:exited', {
|
|
118
|
+
sessionId,
|
|
119
|
+
exitCode,
|
|
120
|
+
signal: signal?.toString(),
|
|
121
|
+
duration: Date.now() - ms.session.createdAt.getTime(),
|
|
122
|
+
finalLineCount: ms.session.lineCount,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
sessionId,
|
|
130
|
+
pid: ptyProcess.pid,
|
|
131
|
+
command: input.command || '',
|
|
132
|
+
status: 'running',
|
|
133
|
+
message: 'Session created and command started',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Execute a command synchronously and wait for completion
|
|
139
|
+
*/
|
|
140
|
+
async exec(input: BashExecInput): Promise<BashExecResponse> {
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
const shell = this.defaultShell;
|
|
143
|
+
const cwd = input.cwd || process.cwd();
|
|
144
|
+
const timeout = Math.min(input.timeout || 30000, 600000); // Max 10 minutes
|
|
145
|
+
const env = { ...process.env, ...input.env } as Record<string, string>;
|
|
146
|
+
|
|
147
|
+
let stdout = '';
|
|
148
|
+
let stderr = '';
|
|
149
|
+
let timedOut = false;
|
|
150
|
+
const startTime = Date.now();
|
|
151
|
+
|
|
152
|
+
const ptyProcess = pty.spawn(shell, ['-c', input.command], {
|
|
153
|
+
name: 'xterm-256color',
|
|
154
|
+
cols: 120,
|
|
155
|
+
rows: 30,
|
|
156
|
+
cwd,
|
|
157
|
+
env,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Timeout handler
|
|
161
|
+
const timeoutId = setTimeout(() => {
|
|
162
|
+
timedOut = true;
|
|
163
|
+
ptyProcess.kill();
|
|
164
|
+
}, timeout);
|
|
165
|
+
|
|
166
|
+
// Collect output
|
|
167
|
+
ptyProcess.onData((data: string) => {
|
|
168
|
+
stdout += data;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Handle exit
|
|
172
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
173
|
+
clearTimeout(timeoutId);
|
|
174
|
+
resolve({
|
|
175
|
+
exitCode: timedOut ? -1 : exitCode,
|
|
176
|
+
stdout: stdout.trim(),
|
|
177
|
+
stderr: stderr.trim(),
|
|
178
|
+
duration: Date.now() - startTime,
|
|
179
|
+
timedOut,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Write data to a PTY session
|
|
187
|
+
*/
|
|
188
|
+
write(input: BashWriteInput): BashWriteResponse {
|
|
189
|
+
const ms = this.sessions.get(input.sessionId);
|
|
190
|
+
if (!ms) {
|
|
191
|
+
throw new Error(`Session not found: ${input.sessionId}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (ms.session.status !== 'running') {
|
|
195
|
+
throw new Error(`Session is not running: ${input.sessionId}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let data = input.data || '';
|
|
199
|
+
if (input.specialKey) {
|
|
200
|
+
data = SPECIAL_KEYS[input.specialKey] || '';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!data) {
|
|
204
|
+
throw new Error('No data or specialKey provided');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
ms.ptyProcess.write(data);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
success: true,
|
|
211
|
+
bytesWritten: data.length,
|
|
212
|
+
sessionStatus: ms.session.status,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Read output from a PTY session
|
|
218
|
+
*/
|
|
219
|
+
read(input: BashReadInput): BashReadResponse {
|
|
220
|
+
const ms = this.sessions.get(input.sessionId);
|
|
221
|
+
if (!ms) {
|
|
222
|
+
throw new Error(`Session not found: ${input.sessionId}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let lines = [...ms.outputBuffer];
|
|
226
|
+
|
|
227
|
+
// Filter by pattern
|
|
228
|
+
if (input.pattern) {
|
|
229
|
+
const regex = new RegExp(input.pattern, input.ignoreCase ? 'i' : '');
|
|
230
|
+
lines = lines.filter((line) => regex.test(line.text));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Filter by timestamp
|
|
234
|
+
if (input.since) {
|
|
235
|
+
const sinceDate = new Date(input.since);
|
|
236
|
+
lines = lines.filter((line) => line.timestamp >= sinceDate);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const totalLines = lines.length;
|
|
240
|
+
|
|
241
|
+
// Apply pagination
|
|
242
|
+
const offset = input.offset || 0;
|
|
243
|
+
const limit = Math.min(input.limit || 100, 1000);
|
|
244
|
+
|
|
245
|
+
if (input.tail) {
|
|
246
|
+
// Return last N lines
|
|
247
|
+
lines = lines.slice(Math.max(0, lines.length - limit));
|
|
248
|
+
} else {
|
|
249
|
+
lines = lines.slice(offset, offset + limit);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
sessionId: input.sessionId,
|
|
254
|
+
lines: lines.map((l, idx) => ({
|
|
255
|
+
line: input.tail ? totalLines - lines.length + idx + 1 : offset + idx + 1,
|
|
256
|
+
text: l.text,
|
|
257
|
+
timestamp: l.timestamp.toISOString(),
|
|
258
|
+
})),
|
|
259
|
+
totalLines: ms.session.lineCount,
|
|
260
|
+
hasMore: offset + limit < totalLines,
|
|
261
|
+
sessionStatus: ms.session.status,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* List all PTY sessions
|
|
267
|
+
*/
|
|
268
|
+
list(input: BashListInput = {}): BashListResponse {
|
|
269
|
+
const sessions: BashListResponse['sessions'] = [];
|
|
270
|
+
let running = 0,
|
|
271
|
+
exited = 0,
|
|
272
|
+
killed = 0;
|
|
273
|
+
|
|
274
|
+
for (const [, ms] of this.sessions) {
|
|
275
|
+
// Count by status
|
|
276
|
+
if (ms.session.status === 'running') running++;
|
|
277
|
+
else if (ms.session.status === 'exited') exited++;
|
|
278
|
+
else if (ms.session.status === 'killed') killed++;
|
|
279
|
+
|
|
280
|
+
// Filter by status
|
|
281
|
+
if (input.status && input.status !== 'all' && ms.session.status !== input.status) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const sessionInfo: BashListResponse['sessions'][0] = {
|
|
286
|
+
sessionId: ms.session.id,
|
|
287
|
+
name: ms.session.name,
|
|
288
|
+
pid: ms.session.pid,
|
|
289
|
+
command: ms.session.command,
|
|
290
|
+
status: ms.session.status,
|
|
291
|
+
createdAt: ms.session.createdAt.toISOString(),
|
|
292
|
+
lineCount: ms.session.lineCount,
|
|
293
|
+
cwd: ms.session.cwd,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Include recent output if requested
|
|
297
|
+
if (input.includeOutput) {
|
|
298
|
+
const outputLines = input.outputLines || 5;
|
|
299
|
+
sessionInfo.recentOutput = ms.outputBuffer
|
|
300
|
+
.slice(-outputLines)
|
|
301
|
+
.map((l) => l.text);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
sessions.push(sessionInfo);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
sessions,
|
|
309
|
+
summary: {
|
|
310
|
+
total: this.sessions.size,
|
|
311
|
+
running,
|
|
312
|
+
exited,
|
|
313
|
+
killed,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Kill a PTY session
|
|
320
|
+
*/
|
|
321
|
+
async kill(input: BashKillInput): Promise<BashKillResponse> {
|
|
322
|
+
const ms = this.sessions.get(input.sessionId);
|
|
323
|
+
if (!ms) {
|
|
324
|
+
throw new Error(`Session not found: ${input.sessionId}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const signal = input.signal || 'SIGTERM';
|
|
328
|
+
const gracePeriod = input.gracePeriod || 5000;
|
|
329
|
+
const finalLineCount = ms.session.lineCount;
|
|
330
|
+
|
|
331
|
+
if (ms.session.status === 'running') {
|
|
332
|
+
// Send signal
|
|
333
|
+
ms.ptyProcess.kill(signal);
|
|
334
|
+
|
|
335
|
+
// Wait for graceful termination
|
|
336
|
+
if (signal === 'SIGTERM') {
|
|
337
|
+
await new Promise<void>((resolve) => {
|
|
338
|
+
const checkInterval = setInterval(() => {
|
|
339
|
+
if (ms.session.status !== 'running') {
|
|
340
|
+
clearInterval(checkInterval);
|
|
341
|
+
resolve();
|
|
342
|
+
}
|
|
343
|
+
}, 100);
|
|
344
|
+
|
|
345
|
+
setTimeout(() => {
|
|
346
|
+
clearInterval(checkInterval);
|
|
347
|
+
if (ms.session.status === 'running') {
|
|
348
|
+
ms.ptyProcess.kill('SIGKILL');
|
|
349
|
+
}
|
|
350
|
+
resolve();
|
|
351
|
+
}, gracePeriod);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
ms.session.status = 'killed';
|
|
356
|
+
ms.session.signal = signal;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Cleanup if requested
|
|
360
|
+
if (input.cleanup) {
|
|
361
|
+
this.sessions.delete(input.sessionId);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
sessionId: input.sessionId,
|
|
366
|
+
exitCode: ms.session.exitCode,
|
|
367
|
+
signal,
|
|
368
|
+
cleaned: input.cleanup || false,
|
|
369
|
+
finalLineCount,
|
|
370
|
+
message: input.cleanup
|
|
371
|
+
? 'Session terminated and cleaned up'
|
|
372
|
+
: 'Session terminated successfully',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Get a session by ID
|
|
378
|
+
*/
|
|
379
|
+
getSession(sessionId: string): PTYSession | undefined {
|
|
380
|
+
return this.sessions.get(sessionId)?.session;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Shutdown all sessions
|
|
385
|
+
*/
|
|
386
|
+
async shutdown(): Promise<void> {
|
|
387
|
+
const promises: Promise<BashKillResponse>[] = [];
|
|
388
|
+
for (const [sessionId, ms] of this.sessions) {
|
|
389
|
+
if (ms.session.status === 'running') {
|
|
390
|
+
promises.push(this.kill({ sessionId, cleanup: true }));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
await Promise.all(promises);
|
|
394
|
+
this.sessions.clear();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Append output to a session's buffer
|
|
399
|
+
*/
|
|
400
|
+
private appendOutput(sessionId: string, data: string): void {
|
|
401
|
+
const ms = this.sessions.get(sessionId);
|
|
402
|
+
if (!ms) return;
|
|
403
|
+
|
|
404
|
+
// Split by newlines and add to buffer
|
|
405
|
+
const lines = data.split(/\r?\n/);
|
|
406
|
+
const now = new Date();
|
|
407
|
+
|
|
408
|
+
for (const text of lines) {
|
|
409
|
+
if (text || lines.length === 1) {
|
|
410
|
+
ms.outputBuffer.push({
|
|
411
|
+
line: ms.session.lineCount + 1,
|
|
412
|
+
text,
|
|
413
|
+
timestamp: now,
|
|
414
|
+
});
|
|
415
|
+
ms.session.lineCount++;
|
|
416
|
+
|
|
417
|
+
// Trim buffer if too large
|
|
418
|
+
if (ms.outputBuffer.length > this.maxOutputLines) {
|
|
419
|
+
ms.outputBuffer.shift();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Emit output event
|
|
425
|
+
this.emit('output', { sessionId, data });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Export singleton instance
|
|
430
|
+
export const ptyManager = new PTYManager();
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Definitions for Seahorse Bash Client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ToolDefinition } from './types';
|
|
6
|
+
|
|
7
|
+
export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
8
|
+
{
|
|
9
|
+
name: 'bash_spawn',
|
|
10
|
+
description:
|
|
11
|
+
'Create a new PTY session and run a command in background. Use this for long-running processes like dev servers, watchers, or any command that needs persistent terminal state.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
command: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: "Command to execute (e.g., 'npm run dev', 'python server.py')",
|
|
18
|
+
},
|
|
19
|
+
args: {
|
|
20
|
+
type: 'array',
|
|
21
|
+
items: { type: 'string' },
|
|
22
|
+
description: 'Command arguments (optional)',
|
|
23
|
+
},
|
|
24
|
+
cwd: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Working directory (default: current directory)',
|
|
27
|
+
},
|
|
28
|
+
env: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
additionalProperties: { type: 'string' },
|
|
31
|
+
description: 'Additional environment variables',
|
|
32
|
+
},
|
|
33
|
+
name: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Session name/identifier (optional, auto-generated if not provided)',
|
|
36
|
+
},
|
|
37
|
+
shell: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
enum: ['bash', 'sh', 'zsh'],
|
|
40
|
+
default: 'bash',
|
|
41
|
+
description: 'Shell to use',
|
|
42
|
+
},
|
|
43
|
+
cols: {
|
|
44
|
+
type: 'integer',
|
|
45
|
+
default: 120,
|
|
46
|
+
description: 'Terminal columns',
|
|
47
|
+
},
|
|
48
|
+
rows: {
|
|
49
|
+
type: 'integer',
|
|
50
|
+
default: 30,
|
|
51
|
+
description: 'Terminal rows',
|
|
52
|
+
},
|
|
53
|
+
notifyOnExit: {
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
default: true,
|
|
56
|
+
description: 'Whether to send notification when process exits',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
required: ['command'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'bash_exec',
|
|
64
|
+
description:
|
|
65
|
+
'Execute a command synchronously and wait for completion. Use this for quick commands that should finish within the timeout. Output is returned directly.',
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
command: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'Bash command to execute',
|
|
72
|
+
},
|
|
73
|
+
cwd: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description: 'Working directory (optional)',
|
|
76
|
+
},
|
|
77
|
+
timeout: {
|
|
78
|
+
type: 'integer',
|
|
79
|
+
default: 30000,
|
|
80
|
+
description: 'Timeout in milliseconds (default 30s, max 600s)',
|
|
81
|
+
},
|
|
82
|
+
env: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
additionalProperties: { type: 'string' },
|
|
85
|
+
description: 'Additional environment variables',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
required: ['command'],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'bash_write',
|
|
93
|
+
description:
|
|
94
|
+
'Write data or send special keys to a running PTY session. Use this to send input to interactive programs, including Ctrl+C to interrupt.',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
sessionId: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'Target session ID',
|
|
101
|
+
},
|
|
102
|
+
data: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
description: 'Text data to send',
|
|
105
|
+
},
|
|
106
|
+
specialKey: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
enum: ['ctrl+c', 'ctrl+d', 'ctrl+z', 'enter', 'tab', 'up', 'down', 'left', 'right'],
|
|
109
|
+
description: 'Special key to send (alternative to data)',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ['sessionId'],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'bash_read',
|
|
117
|
+
description:
|
|
118
|
+
'Read output from a PTY session buffer. Supports pagination, filtering by regex pattern, and retrieving output since a specific timestamp.',
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
sessionId: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'Target session ID',
|
|
125
|
+
},
|
|
126
|
+
offset: {
|
|
127
|
+
type: 'integer',
|
|
128
|
+
default: 0,
|
|
129
|
+
description: 'Starting line number (0-indexed)',
|
|
130
|
+
},
|
|
131
|
+
limit: {
|
|
132
|
+
type: 'integer',
|
|
133
|
+
default: 100,
|
|
134
|
+
maximum: 1000,
|
|
135
|
+
description: 'Maximum lines to return',
|
|
136
|
+
},
|
|
137
|
+
pattern: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: 'Regex pattern to filter lines (optional)',
|
|
140
|
+
},
|
|
141
|
+
ignoreCase: {
|
|
142
|
+
type: 'boolean',
|
|
143
|
+
default: false,
|
|
144
|
+
description: 'Case-insensitive pattern matching',
|
|
145
|
+
},
|
|
146
|
+
since: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
format: 'date-time',
|
|
149
|
+
description: 'Return output after this timestamp (optional)',
|
|
150
|
+
},
|
|
151
|
+
tail: {
|
|
152
|
+
type: 'boolean',
|
|
153
|
+
default: false,
|
|
154
|
+
description: 'If true, return last N lines instead of from offset',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
required: ['sessionId'],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'bash_list',
|
|
162
|
+
description:
|
|
163
|
+
'List all PTY sessions with their status. Optionally include recent output from each session.',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
status: {
|
|
168
|
+
type: 'string',
|
|
169
|
+
enum: ['all', 'running', 'exited', 'killed'],
|
|
170
|
+
default: 'all',
|
|
171
|
+
description: 'Filter by session status',
|
|
172
|
+
},
|
|
173
|
+
includeOutput: {
|
|
174
|
+
type: 'boolean',
|
|
175
|
+
default: false,
|
|
176
|
+
description: 'Include recent output lines',
|
|
177
|
+
},
|
|
178
|
+
outputLines: {
|
|
179
|
+
type: 'integer',
|
|
180
|
+
default: 5,
|
|
181
|
+
description: 'Number of output lines to include if includeOutput=true',
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'bash_kill',
|
|
188
|
+
description:
|
|
189
|
+
'Terminate a PTY session. Sends SIGTERM by default with graceful shutdown, then SIGKILL if needed.',
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: 'object',
|
|
192
|
+
properties: {
|
|
193
|
+
sessionId: {
|
|
194
|
+
type: 'string',
|
|
195
|
+
description: 'Session ID to terminate',
|
|
196
|
+
},
|
|
197
|
+
signal: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
enum: ['SIGTERM', 'SIGKILL', 'SIGINT'],
|
|
200
|
+
default: 'SIGTERM',
|
|
201
|
+
description: 'Signal to send',
|
|
202
|
+
},
|
|
203
|
+
cleanup: {
|
|
204
|
+
type: 'boolean',
|
|
205
|
+
default: false,
|
|
206
|
+
description: 'If true, remove session from list (deletes output buffer)',
|
|
207
|
+
},
|
|
208
|
+
gracePeriod: {
|
|
209
|
+
type: 'integer',
|
|
210
|
+
default: 5000,
|
|
211
|
+
description: 'Time to wait before SIGKILL after SIGTERM (ms)',
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
required: ['sessionId'],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
];
|