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
package/src/types.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seahorse Bash Client Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// PTY Session Types
|
|
6
|
+
export type SessionStatus = 'running' | 'exited' | 'killed';
|
|
7
|
+
|
|
8
|
+
export interface PTYSession {
|
|
9
|
+
id: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
pid: number;
|
|
12
|
+
command: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
cwd: string;
|
|
15
|
+
status: SessionStatus;
|
|
16
|
+
createdAt: Date;
|
|
17
|
+
exitCode?: number;
|
|
18
|
+
signal?: string;
|
|
19
|
+
lineCount: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface OutputLine {
|
|
23
|
+
line: number;
|
|
24
|
+
text: string;
|
|
25
|
+
timestamp: Date;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Tool Input Types
|
|
29
|
+
export interface BashSpawnInput {
|
|
30
|
+
command?: string;
|
|
31
|
+
args?: string[];
|
|
32
|
+
cwd?: string;
|
|
33
|
+
env?: Record<string, string>;
|
|
34
|
+
name?: string;
|
|
35
|
+
shell?: 'bash' | 'sh' | 'zsh';
|
|
36
|
+
cols?: number;
|
|
37
|
+
rows?: number;
|
|
38
|
+
notifyOnExit?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface BashExecInput {
|
|
42
|
+
command: string;
|
|
43
|
+
cwd?: string;
|
|
44
|
+
timeout?: number;
|
|
45
|
+
env?: Record<string, string>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BashWriteInput {
|
|
49
|
+
sessionId: string;
|
|
50
|
+
data?: string;
|
|
51
|
+
specialKey?: 'ctrl+c' | 'ctrl+d' | 'ctrl+z' | 'enter' | 'tab' | 'up' | 'down' | 'left' | 'right';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface BashReadInput {
|
|
55
|
+
sessionId: string;
|
|
56
|
+
offset?: number;
|
|
57
|
+
limit?: number;
|
|
58
|
+
pattern?: string;
|
|
59
|
+
ignoreCase?: boolean;
|
|
60
|
+
since?: string;
|
|
61
|
+
tail?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BashListInput {
|
|
65
|
+
status?: 'all' | 'running' | 'exited' | 'killed';
|
|
66
|
+
includeOutput?: boolean;
|
|
67
|
+
outputLines?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface BashKillInput {
|
|
71
|
+
sessionId: string;
|
|
72
|
+
signal?: 'SIGTERM' | 'SIGKILL' | 'SIGINT';
|
|
73
|
+
cleanup?: boolean;
|
|
74
|
+
gracePeriod?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Tool Response Types
|
|
78
|
+
export interface BashSpawnResponse {
|
|
79
|
+
sessionId: string;
|
|
80
|
+
pid: number;
|
|
81
|
+
command: string;
|
|
82
|
+
status: SessionStatus;
|
|
83
|
+
message: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface BashExecResponse {
|
|
87
|
+
exitCode: number;
|
|
88
|
+
stdout: string;
|
|
89
|
+
stderr: string;
|
|
90
|
+
duration: number;
|
|
91
|
+
timedOut: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface BashWriteResponse {
|
|
95
|
+
success: boolean;
|
|
96
|
+
bytesWritten: number;
|
|
97
|
+
sessionStatus: SessionStatus;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface BashReadResponse {
|
|
101
|
+
sessionId: string;
|
|
102
|
+
lines: Array<{ line: number; text: string; timestamp: string }>;
|
|
103
|
+
totalLines: number;
|
|
104
|
+
hasMore: boolean;
|
|
105
|
+
sessionStatus: SessionStatus;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface BashListResponse {
|
|
109
|
+
sessions: Array<{
|
|
110
|
+
sessionId: string;
|
|
111
|
+
name?: string;
|
|
112
|
+
pid: number;
|
|
113
|
+
command: string;
|
|
114
|
+
status: SessionStatus;
|
|
115
|
+
createdAt: string;
|
|
116
|
+
lineCount: number;
|
|
117
|
+
cwd: string;
|
|
118
|
+
recentOutput?: string[];
|
|
119
|
+
}>;
|
|
120
|
+
summary: {
|
|
121
|
+
total: number;
|
|
122
|
+
running: number;
|
|
123
|
+
exited: number;
|
|
124
|
+
killed: number;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface BashKillResponse {
|
|
129
|
+
sessionId: string;
|
|
130
|
+
exitCode?: number;
|
|
131
|
+
signal: string;
|
|
132
|
+
cleaned: boolean;
|
|
133
|
+
finalLineCount: number;
|
|
134
|
+
message: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// WebSocket Protocol Types
|
|
138
|
+
export interface ToolDefinition {
|
|
139
|
+
name: string;
|
|
140
|
+
description: string;
|
|
141
|
+
inputSchema: object;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface RegisterMessage {
|
|
145
|
+
type: 'registration';
|
|
146
|
+
tools: ToolDefinition[];
|
|
147
|
+
metadata: {
|
|
148
|
+
version: string;
|
|
149
|
+
hostname: string;
|
|
150
|
+
platform: string;
|
|
151
|
+
shell: string;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface ToolCallMessage {
|
|
156
|
+
type: 'tool_call';
|
|
157
|
+
request_id: string;
|
|
158
|
+
method: string;
|
|
159
|
+
params: {
|
|
160
|
+
name: string;
|
|
161
|
+
arguments: Record<string, unknown>;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ToolResponseMessage {
|
|
166
|
+
type: 'tool_response';
|
|
167
|
+
request_id: string;
|
|
168
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
169
|
+
isError: boolean;
|
|
170
|
+
error?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface NotificationMessage {
|
|
174
|
+
type: 'notification';
|
|
175
|
+
method: string;
|
|
176
|
+
params: Record<string, unknown>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface HeartbeatMessage {
|
|
180
|
+
type: 'heartbeat';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface HeartbeatAckMessage {
|
|
184
|
+
type: 'heartbeat_ack';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface RegistrationAckMessage {
|
|
188
|
+
type: 'registration_ack';
|
|
189
|
+
server_name: string;
|
|
190
|
+
registered_tools: number;
|
|
191
|
+
tool_names: string[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ErrorMessage {
|
|
195
|
+
type: 'error';
|
|
196
|
+
error: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export type WebSocketMessage =
|
|
200
|
+
| RegisterMessage
|
|
201
|
+
| ToolCallMessage
|
|
202
|
+
| ToolResponseMessage
|
|
203
|
+
| NotificationMessage
|
|
204
|
+
| HeartbeatMessage
|
|
205
|
+
| HeartbeatAckMessage
|
|
206
|
+
| RegistrationAckMessage
|
|
207
|
+
| ErrorMessage;
|
|
208
|
+
|
|
209
|
+
// Configuration
|
|
210
|
+
export interface ClientConfig {
|
|
211
|
+
serverUrl: string;
|
|
212
|
+
serverName: string;
|
|
213
|
+
reconnectInterval?: number;
|
|
214
|
+
heartbeatInterval?: number;
|
|
215
|
+
maxOutputLines?: number;
|
|
216
|
+
defaultShell?: string;
|
|
217
|
+
allowedCommands?: string[];
|
|
218
|
+
blockedCommands?: string[];
|
|
219
|
+
allowedCwdPaths?: string[];
|
|
220
|
+
insecure?: boolean; // Skip SSL certificate verification
|
|
221
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Client - Connects to Seahorse Agent and handles MCP tool calls
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import WebSocket from 'ws';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import https from 'https';
|
|
9
|
+
import http from 'http';
|
|
10
|
+
import {
|
|
11
|
+
ClientConfig,
|
|
12
|
+
RegisterMessage,
|
|
13
|
+
ToolCallMessage,
|
|
14
|
+
ToolResponseMessage,
|
|
15
|
+
NotificationMessage,
|
|
16
|
+
WebSocketMessage,
|
|
17
|
+
} from './types';
|
|
18
|
+
import { PTYManager } from './pty-manager';
|
|
19
|
+
import { TOOL_DEFINITIONS } from './tools';
|
|
20
|
+
|
|
21
|
+
export class WebSocketClient extends EventEmitter {
|
|
22
|
+
private ws: WebSocket | null = null;
|
|
23
|
+
private config: ClientConfig;
|
|
24
|
+
private ptyManager: PTYManager;
|
|
25
|
+
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
26
|
+
private heartbeatTimer: NodeJS.Timeout | null = null;
|
|
27
|
+
private isConnected = false;
|
|
28
|
+
private isShuttingDown = false;
|
|
29
|
+
|
|
30
|
+
constructor(config: ClientConfig) {
|
|
31
|
+
super();
|
|
32
|
+
this.config = {
|
|
33
|
+
reconnectInterval: 5000,
|
|
34
|
+
heartbeatInterval: 30000,
|
|
35
|
+
maxOutputLines: 50000,
|
|
36
|
+
...config,
|
|
37
|
+
};
|
|
38
|
+
this.ptyManager = new PTYManager({
|
|
39
|
+
maxOutputLines: this.config.maxOutputLines,
|
|
40
|
+
defaultShell: this.config.defaultShell,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Forward session exit events
|
|
44
|
+
this.ptyManager.on('session:exited', (data) => {
|
|
45
|
+
this.sendNotification('session/exited', data);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Connect to the Seahorse Agent
|
|
51
|
+
*/
|
|
52
|
+
async connect(): Promise<void> {
|
|
53
|
+
if (this.isShuttingDown) return;
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(this.config.serverUrl);
|
|
58
|
+
const isSecure = url.protocol === 'wss:';
|
|
59
|
+
|
|
60
|
+
// Create agent that forces HTTP/1.1 (no ALPN for HTTP/2)
|
|
61
|
+
const agent = isSecure
|
|
62
|
+
? new https.Agent({
|
|
63
|
+
rejectUnauthorized: !this.config.insecure,
|
|
64
|
+
// Force HTTP/1.1 by not advertising HTTP/2
|
|
65
|
+
ALPNProtocols: ['http/1.1'],
|
|
66
|
+
})
|
|
67
|
+
: new http.Agent();
|
|
68
|
+
|
|
69
|
+
const wsOptions: WebSocket.ClientOptions = {
|
|
70
|
+
agent,
|
|
71
|
+
perMessageDeflate: false,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
this.ws = new WebSocket(this.config.serverUrl, wsOptions);
|
|
75
|
+
|
|
76
|
+
this.ws.on('open', () => {
|
|
77
|
+
this.isConnected = true;
|
|
78
|
+
this.emit('connected');
|
|
79
|
+
this.register();
|
|
80
|
+
this.startHeartbeat();
|
|
81
|
+
resolve();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.ws.on('message', (data: Buffer) => {
|
|
85
|
+
this.handleMessage(data);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.ws.on('close', (code: number, reason: Buffer) => {
|
|
89
|
+
this.isConnected = false;
|
|
90
|
+
this.stopHeartbeat();
|
|
91
|
+
this.emit('disconnected', { code, reason: reason.toString() });
|
|
92
|
+
|
|
93
|
+
if (!this.isShuttingDown) {
|
|
94
|
+
this.scheduleReconnect();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this.ws.on('error', (error: Error) => {
|
|
99
|
+
this.emit('error', error);
|
|
100
|
+
if (!this.isConnected) {
|
|
101
|
+
reject(error);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
reject(error);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Disconnect from the server
|
|
112
|
+
*/
|
|
113
|
+
async disconnect(): Promise<void> {
|
|
114
|
+
this.isShuttingDown = true;
|
|
115
|
+
|
|
116
|
+
if (this.reconnectTimer) {
|
|
117
|
+
clearTimeout(this.reconnectTimer);
|
|
118
|
+
this.reconnectTimer = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.stopHeartbeat();
|
|
122
|
+
|
|
123
|
+
// Shutdown all PTY sessions
|
|
124
|
+
await this.ptyManager.shutdown();
|
|
125
|
+
|
|
126
|
+
if (this.ws) {
|
|
127
|
+
this.ws.close(1000, 'Client shutdown');
|
|
128
|
+
this.ws = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Register tools with the agent
|
|
134
|
+
*/
|
|
135
|
+
private register(): void {
|
|
136
|
+
// Note: Server expects "registration" type, not "register"
|
|
137
|
+
const message = {
|
|
138
|
+
type: 'registration',
|
|
139
|
+
tools: TOOL_DEFINITIONS,
|
|
140
|
+
metadata: {
|
|
141
|
+
version: '1.0.0',
|
|
142
|
+
hostname: os.hostname(),
|
|
143
|
+
platform: process.platform,
|
|
144
|
+
shell: this.config.defaultShell || process.env.SHELL || '/bin/bash',
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
this.send(message as any);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Handle incoming WebSocket messages
|
|
153
|
+
*/
|
|
154
|
+
private handleMessage(data: Buffer): void {
|
|
155
|
+
try {
|
|
156
|
+
const message: WebSocketMessage = JSON.parse(data.toString());
|
|
157
|
+
|
|
158
|
+
switch (message.type) {
|
|
159
|
+
case 'tool_call':
|
|
160
|
+
this.handleToolCall(message as ToolCallMessage);
|
|
161
|
+
break;
|
|
162
|
+
case 'heartbeat':
|
|
163
|
+
case 'heartbeat_ack':
|
|
164
|
+
// Respond to heartbeat, ignore ack
|
|
165
|
+
if (message.type === 'heartbeat') {
|
|
166
|
+
this.send({ type: 'heartbeat' });
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
case 'registration_ack':
|
|
170
|
+
// Server acknowledged registration
|
|
171
|
+
const ackData = message as { type: string; server_name: string; registered_tools: number; tool_names: string[] };
|
|
172
|
+
this.emit('registered', {
|
|
173
|
+
serverName: ackData.server_name,
|
|
174
|
+
tools: TOOL_DEFINITIONS,
|
|
175
|
+
registeredCount: ackData.registered_tools,
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
case 'error':
|
|
179
|
+
const errorData = message as { type: string; error: string };
|
|
180
|
+
this.emit('error', new Error(`Server error: ${errorData.error}`));
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
this.emit('message', message);
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
this.emit('error', new Error(`Failed to parse message: ${error}`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Handle a tool call from the agent
|
|
192
|
+
*/
|
|
193
|
+
private async handleToolCall(message: ToolCallMessage): Promise<void> {
|
|
194
|
+
const { request_id, params } = message;
|
|
195
|
+
const { name, arguments: args } = params;
|
|
196
|
+
|
|
197
|
+
this.emit('tool:call', { name, args, requestId: request_id });
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const result = await this.executeTool(name, args);
|
|
201
|
+
this.sendToolResponse(request_id, result, false);
|
|
202
|
+
this.emit('tool:success', { name, requestId: request_id });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
205
|
+
this.sendToolResponse(request_id, { error: errorMessage }, true, errorMessage);
|
|
206
|
+
this.emit('tool:error', { name, requestId: request_id, error: errorMessage });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Execute a tool
|
|
212
|
+
*/
|
|
213
|
+
private async executeTool(
|
|
214
|
+
name: string,
|
|
215
|
+
args: Record<string, unknown>
|
|
216
|
+
): Promise<object> {
|
|
217
|
+
// Validate allowed commands if configured
|
|
218
|
+
if (this.config.allowedCommands && name.startsWith('bash_')) {
|
|
219
|
+
const command = (args.command as string) || '';
|
|
220
|
+
const isAllowed = this.config.allowedCommands.some(
|
|
221
|
+
(allowed) => command.startsWith(allowed) || command === allowed
|
|
222
|
+
);
|
|
223
|
+
if (!isAllowed) {
|
|
224
|
+
throw new Error(`Command not in allowed list: ${command}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Validate blocked commands if configured
|
|
229
|
+
if (this.config.blockedCommands && name.startsWith('bash_')) {
|
|
230
|
+
const command = (args.command as string) || '';
|
|
231
|
+
const isBlocked = this.config.blockedCommands.some(
|
|
232
|
+
(blocked) => command.includes(blocked) || command.startsWith(blocked)
|
|
233
|
+
);
|
|
234
|
+
if (isBlocked) {
|
|
235
|
+
throw new Error(`Command is blocked: ${command}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Validate cwd if restrictions are configured
|
|
240
|
+
if (this.config.allowedCwdPaths && args.cwd) {
|
|
241
|
+
const cwd = args.cwd as string;
|
|
242
|
+
const isAllowed = this.config.allowedCwdPaths.some((allowed) => cwd.startsWith(allowed));
|
|
243
|
+
if (!isAllowed) {
|
|
244
|
+
throw new Error(`Working directory not allowed: ${cwd}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
switch (name) {
|
|
249
|
+
case 'bash_spawn':
|
|
250
|
+
return this.ptyManager.spawn(args as any);
|
|
251
|
+
|
|
252
|
+
case 'bash_exec':
|
|
253
|
+
return this.ptyManager.exec(args as any);
|
|
254
|
+
|
|
255
|
+
case 'bash_write':
|
|
256
|
+
return this.ptyManager.write(args as any);
|
|
257
|
+
|
|
258
|
+
case 'bash_read':
|
|
259
|
+
return this.ptyManager.read(args as any);
|
|
260
|
+
|
|
261
|
+
case 'bash_list':
|
|
262
|
+
return this.ptyManager.list(args as any);
|
|
263
|
+
|
|
264
|
+
case 'bash_kill':
|
|
265
|
+
return this.ptyManager.kill(args as any);
|
|
266
|
+
|
|
267
|
+
default:
|
|
268
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Send a tool response
|
|
274
|
+
*/
|
|
275
|
+
private sendToolResponse(
|
|
276
|
+
requestId: string,
|
|
277
|
+
result: object,
|
|
278
|
+
isError: boolean,
|
|
279
|
+
errorMessage?: string
|
|
280
|
+
): void {
|
|
281
|
+
const response: ToolResponseMessage = {
|
|
282
|
+
type: 'tool_response',
|
|
283
|
+
request_id: requestId,
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: 'text',
|
|
287
|
+
text: JSON.stringify(result, null, 2),
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
isError,
|
|
291
|
+
...(errorMessage && { error: errorMessage }),
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
this.send(response);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Send a notification to the agent
|
|
299
|
+
*/
|
|
300
|
+
private sendNotification(method: string, params: Record<string, unknown>): void {
|
|
301
|
+
if (!this.isConnected) return;
|
|
302
|
+
|
|
303
|
+
const notification: NotificationMessage = {
|
|
304
|
+
type: 'notification',
|
|
305
|
+
method,
|
|
306
|
+
params,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
this.send(notification);
|
|
310
|
+
this.emit('notification:sent', { method, params });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Send a message through the WebSocket
|
|
315
|
+
*/
|
|
316
|
+
private send(message: WebSocketMessage): void {
|
|
317
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
318
|
+
this.ws.send(JSON.stringify(message));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Schedule a reconnection attempt
|
|
324
|
+
*/
|
|
325
|
+
private scheduleReconnect(): void {
|
|
326
|
+
if (this.reconnectTimer) return;
|
|
327
|
+
|
|
328
|
+
this.emit('reconnecting', { interval: this.config.reconnectInterval });
|
|
329
|
+
|
|
330
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
331
|
+
this.reconnectTimer = null;
|
|
332
|
+
try {
|
|
333
|
+
await this.connect();
|
|
334
|
+
} catch (error) {
|
|
335
|
+
// Will retry via close handler
|
|
336
|
+
}
|
|
337
|
+
}, this.config.reconnectInterval);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Start heartbeat timer
|
|
342
|
+
*/
|
|
343
|
+
private startHeartbeat(): void {
|
|
344
|
+
this.heartbeatTimer = setInterval(() => {
|
|
345
|
+
if (this.isConnected) {
|
|
346
|
+
this.send({ type: 'heartbeat' });
|
|
347
|
+
}
|
|
348
|
+
}, this.config.heartbeatInterval);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Stop heartbeat timer
|
|
353
|
+
*/
|
|
354
|
+
private stopHeartbeat(): void {
|
|
355
|
+
if (this.heartbeatTimer) {
|
|
356
|
+
clearInterval(this.heartbeatTimer);
|
|
357
|
+
this.heartbeatTimer = null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get connection status
|
|
363
|
+
*/
|
|
364
|
+
get connected(): boolean {
|
|
365
|
+
return this.isConnected;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get PTY manager for direct access
|
|
370
|
+
*/
|
|
371
|
+
get pty(): PTYManager {
|
|
372
|
+
return this.ptyManager;
|
|
373
|
+
}
|
|
374
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"resolveJsonModule": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|