@kadi.build/core 0.9.0 → 0.11.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/README.md +424 -1
- package/agent.json +19 -0
- package/dist/agent-json.d.ts +231 -0
- package/dist/agent-json.d.ts.map +1 -0
- package/dist/agent-json.js +554 -0
- package/dist/agent-json.js.map +1 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +50 -0
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/process-manager.d.ts +235 -0
- package/dist/process-manager.d.ts.map +1 -0
- package/dist/process-manager.js +647 -0
- package/dist/process-manager.js.map +1 -0
- package/dist/stdio-framing.d.ts +88 -0
- package/dist/stdio-framing.d.ts.map +1 -0
- package/dist/stdio-framing.js +194 -0
- package/dist/stdio-framing.js.map +1 -0
- package/dist/transports/stdio.d.ts.map +1 -1
- package/dist/transports/stdio.js +3 -181
- package/dist/transports/stdio.js.map +1 -1
- package/dist/types.d.ts +256 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +107 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +212 -0
- package/dist/utils.js.map +1 -0
- package/package.json +3 -1
- package/scripts/symlink.mjs +131 -0
- package/src/agent-json.ts +655 -0
- package/src/client.ts +56 -0
- package/src/errors.ts +15 -0
- package/src/index.ts +32 -0
- package/src/process-manager.ts +821 -0
- package/src/stdio-framing.ts +227 -0
- package/src/transports/stdio.ts +4 -221
- package/src/types.ts +277 -0
- package/src/utils.ts +246 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio message framing for kadi-core
|
|
3
|
+
*
|
|
4
|
+
* Shared Content-Length framing used by both the stdio transport (ability loading)
|
|
5
|
+
* and the ProcessManager (bridge mode).
|
|
6
|
+
*
|
|
7
|
+
* Protocol (same as LSP - Language Server Protocol):
|
|
8
|
+
* - Each message is prefixed with "Content-Length: <bytes>\r\n\r\n"
|
|
9
|
+
* - The content is JSON (JSON-RPC 2.0 format)
|
|
10
|
+
*
|
|
11
|
+
* This module is language-agnostic — any process that speaks this protocol
|
|
12
|
+
* can communicate with these readers/writers.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Readable, Writable } from 'stream';
|
|
16
|
+
import type { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification } from './types.js';
|
|
17
|
+
import { KadiError } from './errors.js';
|
|
18
|
+
|
|
19
|
+
export type { JsonRpcNotification };
|
|
20
|
+
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
22
|
+
// MESSAGE READER (Content-Length framing)
|
|
23
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Reads JSON-RPC messages from a stream using Content-Length framing.
|
|
27
|
+
*
|
|
28
|
+
* Uses Buffer (not string) for binary safety. Handles partial messages
|
|
29
|
+
* across chunks and recovers from malformed headers.
|
|
30
|
+
*
|
|
31
|
+
* Message format:
|
|
32
|
+
* ```
|
|
33
|
+
* Content-Length: 42\r\n
|
|
34
|
+
* \r\n
|
|
35
|
+
* {"jsonrpc":"2.0","id":1,"result":{...}}
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class StdioMessageReader {
|
|
39
|
+
private buffer = Buffer.alloc(0);
|
|
40
|
+
private static readonly HEADER_END = '\r\n\r\n';
|
|
41
|
+
private static readonly HEADER_END_LENGTH = 4; // \r\n\r\n
|
|
42
|
+
private static readonly CONTENT_LENGTH_MARKER = Buffer.from('Content-Length:');
|
|
43
|
+
private static readonly CONTENT_LENGTH_PATTERN = /Content-Length:\s*(\d+)/;
|
|
44
|
+
|
|
45
|
+
private waiters: Array<{
|
|
46
|
+
id: string | number;
|
|
47
|
+
resolve: (response: JsonRpcResponse) => void;
|
|
48
|
+
reject: (error: Error) => void;
|
|
49
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
50
|
+
}> = [];
|
|
51
|
+
|
|
52
|
+
/** Handler for event notifications */
|
|
53
|
+
private notificationHandler: ((notification: JsonRpcNotification) => void) | null = null;
|
|
54
|
+
|
|
55
|
+
constructor(stream: Readable, private timeoutMs: number = 600000) {
|
|
56
|
+
stream.on('data', (chunk: Buffer) => this.onData(chunk));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set handler for notifications (messages without id).
|
|
61
|
+
*/
|
|
62
|
+
setNotificationHandler(handler: (notification: JsonRpcNotification) => void): void {
|
|
63
|
+
this.notificationHandler = handler;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Handle incoming data from the stream.
|
|
68
|
+
*/
|
|
69
|
+
private onData(chunk: Buffer): void {
|
|
70
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
71
|
+
this.processBuffer();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Process the buffer, extracting complete messages.
|
|
76
|
+
*/
|
|
77
|
+
private processBuffer(): void {
|
|
78
|
+
let message: JsonRpcResponse | null;
|
|
79
|
+
while ((message = this.tryReadSingleMessage()) !== null) {
|
|
80
|
+
this.dispatchMessage(message);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Try to read a single complete message from the buffer.
|
|
86
|
+
* @returns Parsed message, or null if no complete message available
|
|
87
|
+
*/
|
|
88
|
+
private tryReadSingleMessage(): JsonRpcResponse | null {
|
|
89
|
+
// Look for header end marker
|
|
90
|
+
const headerEndIndex = this.buffer.indexOf(StdioMessageReader.HEADER_END);
|
|
91
|
+
if (headerEndIndex === -1) {
|
|
92
|
+
return null; // No complete header yet
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parse Content-Length from header
|
|
96
|
+
const header = this.buffer.slice(0, headerEndIndex).toString('utf8');
|
|
97
|
+
const match = header.match(StdioMessageReader.CONTENT_LENGTH_PATTERN);
|
|
98
|
+
if (!match?.[1]) {
|
|
99
|
+
// Invalid header - skip to next potential header
|
|
100
|
+
this.skipToNextHeader();
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const contentLength = parseInt(match[1], 10);
|
|
105
|
+
const contentStart = headerEndIndex + StdioMessageReader.HEADER_END_LENGTH;
|
|
106
|
+
const contentEnd = contentStart + contentLength;
|
|
107
|
+
|
|
108
|
+
// Check if we have the complete message
|
|
109
|
+
if (this.buffer.length < contentEnd) {
|
|
110
|
+
return null; // Need more data
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Extract and parse JSON
|
|
114
|
+
const content = this.buffer.slice(contentStart, contentEnd).toString('utf8');
|
|
115
|
+
this.buffer = this.buffer.slice(contentEnd); // Remove processed data
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(content) as JsonRpcResponse;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// Invalid JSON - log error so developer knows something's wrong
|
|
121
|
+
console.error('[KADI] Failed to parse message from child process:', error);
|
|
122
|
+
console.error('[KADI] Raw content (truncated):', content.slice(0, 200));
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Skip to next potential header (error recovery).
|
|
129
|
+
* Called when we encounter a malformed header.
|
|
130
|
+
*/
|
|
131
|
+
private skipToNextHeader(): void {
|
|
132
|
+
const nextHeaderIndex = this.buffer.indexOf(StdioMessageReader.CONTENT_LENGTH_MARKER, 1);
|
|
133
|
+
|
|
134
|
+
if (nextHeaderIndex === -1) {
|
|
135
|
+
this.buffer = Buffer.alloc(0); // No more headers, clear buffer
|
|
136
|
+
} else {
|
|
137
|
+
this.buffer = this.buffer.slice(nextHeaderIndex);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Dispatch a parsed message to waiting handlers or notification handler.
|
|
143
|
+
*/
|
|
144
|
+
private dispatchMessage(message: JsonRpcResponse | JsonRpcNotification): void {
|
|
145
|
+
// Check if this is a notification (no id field)
|
|
146
|
+
if (!('id' in message) || message.id === undefined) {
|
|
147
|
+
// It's a notification - route to notification handler
|
|
148
|
+
if (this.notificationHandler) {
|
|
149
|
+
this.notificationHandler(message as JsonRpcNotification);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// It's a response - find and remove waiter for this message id
|
|
155
|
+
const waiterIndex = this.waiters.findIndex((w) => w.id === message.id);
|
|
156
|
+
if (waiterIndex === -1) {
|
|
157
|
+
return; // No one waiting for this response
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// splice returns removed elements - use destructuring to get the waiter
|
|
161
|
+
const [waiter] = this.waiters.splice(waiterIndex, 1);
|
|
162
|
+
if (!waiter) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
clearTimeout(waiter.timeout);
|
|
167
|
+
waiter.resolve(message);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Wait for a response with a specific id.
|
|
172
|
+
*/
|
|
173
|
+
waitForResponse(id: string | number, timeoutOverride?: number): Promise<JsonRpcResponse> {
|
|
174
|
+
const effectiveTimeout = timeoutOverride ?? this.timeoutMs;
|
|
175
|
+
return new Promise((resolve, reject) => {
|
|
176
|
+
const timeout = setTimeout(() => {
|
|
177
|
+
// Remove from waiters
|
|
178
|
+
const index = this.waiters.findIndex((w) => w.id === id);
|
|
179
|
+
if (index !== -1) {
|
|
180
|
+
this.waiters.splice(index, 1);
|
|
181
|
+
}
|
|
182
|
+
reject(new KadiError(
|
|
183
|
+
`Timeout waiting for response to request ${id}`,
|
|
184
|
+
'TIMEOUT',
|
|
185
|
+
{ requestId: id, timeoutMs: effectiveTimeout }
|
|
186
|
+
));
|
|
187
|
+
}, effectiveTimeout);
|
|
188
|
+
|
|
189
|
+
this.waiters.push({ id, resolve, reject, timeout });
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Cancel all pending waiters (for cleanup).
|
|
195
|
+
* @param error - Optional error to reject with (defaults to generic close error)
|
|
196
|
+
*/
|
|
197
|
+
cancelAll(error?: Error): void {
|
|
198
|
+
const rejectError = error ?? new KadiError('Connection closed', 'STDIO_ERROR');
|
|
199
|
+
for (const waiter of this.waiters) {
|
|
200
|
+
clearTimeout(waiter.timeout);
|
|
201
|
+
waiter.reject(rejectError);
|
|
202
|
+
}
|
|
203
|
+
this.waiters = [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
208
|
+
// MESSAGE WRITER (Content-Length framing)
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Writes JSON-RPC messages to a stream using Content-Length framing.
|
|
213
|
+
*/
|
|
214
|
+
export class StdioMessageWriter {
|
|
215
|
+
constructor(private stream: Writable) {}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Write a message to the stream.
|
|
219
|
+
*/
|
|
220
|
+
write(message: JsonRpcRequest): void {
|
|
221
|
+
const json = JSON.stringify(message);
|
|
222
|
+
const contentLength = Buffer.byteLength(json, 'utf-8');
|
|
223
|
+
const header = `Content-Length: ${contentLength}\r\n\r\n`;
|
|
224
|
+
|
|
225
|
+
this.stream.write(header + json);
|
|
226
|
+
}
|
|
227
|
+
}
|
package/src/transports/stdio.ts
CHANGED
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { spawn, type ChildProcess } from 'child_process';
|
|
22
|
-
import type { Readable, Writable } from 'stream';
|
|
23
22
|
import { z } from 'zod';
|
|
24
|
-
import type { LoadedAbility, InvokeOptions, ToolDefinition,
|
|
23
|
+
import type { LoadedAbility, InvokeOptions, ToolDefinition, EventHandler } from '../types.js';
|
|
25
24
|
import { KadiError } from '../errors.js';
|
|
26
25
|
import * as protocol from '../protocol.js';
|
|
26
|
+
import { StdioMessageReader, StdioMessageWriter } from '../stdio-framing.js';
|
|
27
27
|
|
|
28
28
|
/** Schema for event notification params */
|
|
29
29
|
const EventNotificationParams = z.object({
|
|
@@ -31,223 +31,6 @@ const EventNotificationParams = z.object({
|
|
|
31
31
|
data: z.unknown(),
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
* JSON-RPC notification (no id field).
|
|
36
|
-
*/
|
|
37
|
-
interface JsonRpcNotification {
|
|
38
|
-
jsonrpc: '2.0';
|
|
39
|
-
method: string;
|
|
40
|
-
params?: unknown;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
44
|
-
// MESSAGE READER (Content-Length framing)
|
|
45
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Reads JSON-RPC messages from a stream using Content-Length framing.
|
|
49
|
-
*
|
|
50
|
-
* Uses Buffer (not string) for binary safety. Handles partial messages
|
|
51
|
-
* across chunks and recovers from malformed headers.
|
|
52
|
-
*
|
|
53
|
-
* Message format:
|
|
54
|
-
* ```
|
|
55
|
-
* Content-Length: 42\r\n
|
|
56
|
-
* \r\n
|
|
57
|
-
* {"jsonrpc":"2.0","id":1,"result":{...}}
|
|
58
|
-
* ```
|
|
59
|
-
*/
|
|
60
|
-
class StdioReader {
|
|
61
|
-
private buffer = Buffer.alloc(0);
|
|
62
|
-
private static readonly HEADER_END = '\r\n\r\n';
|
|
63
|
-
private static readonly HEADER_END_LENGTH = 4; // \r\n\r\n
|
|
64
|
-
private static readonly CONTENT_LENGTH_MARKER = Buffer.from('Content-Length:');
|
|
65
|
-
private static readonly CONTENT_LENGTH_PATTERN = /Content-Length:\s*(\d+)/;
|
|
66
|
-
|
|
67
|
-
private waiters: Array<{
|
|
68
|
-
id: string | number;
|
|
69
|
-
resolve: (response: JsonRpcResponse) => void;
|
|
70
|
-
reject: (error: Error) => void;
|
|
71
|
-
timeout: ReturnType<typeof setTimeout>;
|
|
72
|
-
}> = [];
|
|
73
|
-
|
|
74
|
-
/** Handler for event notifications */
|
|
75
|
-
private notificationHandler: ((notification: JsonRpcNotification) => void) | null = null;
|
|
76
|
-
|
|
77
|
-
constructor(stream: Readable, private timeoutMs: number = 600000) {
|
|
78
|
-
stream.on('data', (chunk: Buffer) => this.onData(chunk));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Set handler for notifications (messages without id).
|
|
83
|
-
*/
|
|
84
|
-
setNotificationHandler(handler: (notification: JsonRpcNotification) => void): void {
|
|
85
|
-
this.notificationHandler = handler;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Handle incoming data from the stream.
|
|
90
|
-
*/
|
|
91
|
-
private onData(chunk: Buffer): void {
|
|
92
|
-
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
93
|
-
this.processBuffer();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Process the buffer, extracting complete messages.
|
|
98
|
-
*/
|
|
99
|
-
private processBuffer(): void {
|
|
100
|
-
let message: JsonRpcResponse | null;
|
|
101
|
-
while ((message = this.tryReadSingleMessage()) !== null) {
|
|
102
|
-
this.dispatchMessage(message);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Try to read a single complete message from the buffer.
|
|
108
|
-
* @returns Parsed message, or null if no complete message available
|
|
109
|
-
*/
|
|
110
|
-
private tryReadSingleMessage(): JsonRpcResponse | null {
|
|
111
|
-
// Look for header end marker
|
|
112
|
-
const headerEndIndex = this.buffer.indexOf(StdioReader.HEADER_END);
|
|
113
|
-
if (headerEndIndex === -1) {
|
|
114
|
-
return null; // No complete header yet
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Parse Content-Length from header
|
|
118
|
-
const header = this.buffer.slice(0, headerEndIndex).toString('utf8');
|
|
119
|
-
const match = header.match(StdioReader.CONTENT_LENGTH_PATTERN);
|
|
120
|
-
if (!match?.[1]) {
|
|
121
|
-
// Invalid header - skip to next potential header
|
|
122
|
-
this.skipToNextHeader();
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const contentLength = parseInt(match[1], 10);
|
|
127
|
-
const contentStart = headerEndIndex + StdioReader.HEADER_END_LENGTH;
|
|
128
|
-
const contentEnd = contentStart + contentLength;
|
|
129
|
-
|
|
130
|
-
// Check if we have the complete message
|
|
131
|
-
if (this.buffer.length < contentEnd) {
|
|
132
|
-
return null; // Need more data
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Extract and parse JSON
|
|
136
|
-
const content = this.buffer.slice(contentStart, contentEnd).toString('utf8');
|
|
137
|
-
this.buffer = this.buffer.slice(contentEnd); // Remove processed data
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
return JSON.parse(content) as JsonRpcResponse;
|
|
141
|
-
} catch (error) {
|
|
142
|
-
// Invalid JSON - log error so developer knows something's wrong
|
|
143
|
-
console.error('[KADI] Failed to parse message from child process:', error);
|
|
144
|
-
console.error('[KADI] Raw content (truncated):', content.slice(0, 200));
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Skip to next potential header (error recovery).
|
|
151
|
-
* Called when we encounter a malformed header.
|
|
152
|
-
*/
|
|
153
|
-
private skipToNextHeader(): void {
|
|
154
|
-
const nextHeaderIndex = this.buffer.indexOf(StdioReader.CONTENT_LENGTH_MARKER, 1);
|
|
155
|
-
|
|
156
|
-
if (nextHeaderIndex === -1) {
|
|
157
|
-
this.buffer = Buffer.alloc(0); // No more headers, clear buffer
|
|
158
|
-
} else {
|
|
159
|
-
this.buffer = this.buffer.slice(nextHeaderIndex);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Dispatch a parsed message to waiting handlers or notification handler.
|
|
165
|
-
*/
|
|
166
|
-
private dispatchMessage(message: JsonRpcResponse | JsonRpcNotification): void {
|
|
167
|
-
// Check if this is a notification (no id field)
|
|
168
|
-
if (!('id' in message) || message.id === undefined) {
|
|
169
|
-
// It's a notification - route to notification handler
|
|
170
|
-
if (this.notificationHandler) {
|
|
171
|
-
this.notificationHandler(message as JsonRpcNotification);
|
|
172
|
-
}
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// It's a response - find and remove waiter for this message id
|
|
177
|
-
const waiterIndex = this.waiters.findIndex((w) => w.id === message.id);
|
|
178
|
-
if (waiterIndex === -1) {
|
|
179
|
-
return; // No one waiting for this response
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// splice returns removed elements - use destructuring to get the waiter
|
|
183
|
-
const [waiter] = this.waiters.splice(waiterIndex, 1);
|
|
184
|
-
if (!waiter) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
clearTimeout(waiter.timeout);
|
|
189
|
-
waiter.resolve(message);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Wait for a response with a specific id.
|
|
194
|
-
*/
|
|
195
|
-
waitForResponse(id: string | number, timeoutOverride?: number): Promise<JsonRpcResponse> {
|
|
196
|
-
const effectiveTimeout = timeoutOverride ?? this.timeoutMs;
|
|
197
|
-
return new Promise((resolve, reject) => {
|
|
198
|
-
const timeout = setTimeout(() => {
|
|
199
|
-
// Remove from waiters
|
|
200
|
-
const index = this.waiters.findIndex((w) => w.id === id);
|
|
201
|
-
if (index !== -1) {
|
|
202
|
-
this.waiters.splice(index, 1);
|
|
203
|
-
}
|
|
204
|
-
reject(new KadiError(
|
|
205
|
-
`Timeout waiting for response to request ${id}`,
|
|
206
|
-
'TIMEOUT',
|
|
207
|
-
{ requestId: id, timeoutMs: effectiveTimeout }
|
|
208
|
-
));
|
|
209
|
-
}, effectiveTimeout);
|
|
210
|
-
|
|
211
|
-
this.waiters.push({ id, resolve, reject, timeout });
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Cancel all pending waiters (for cleanup).
|
|
217
|
-
* @param error - Optional error to reject with (defaults to generic close error)
|
|
218
|
-
*/
|
|
219
|
-
cancelAll(error?: Error): void {
|
|
220
|
-
const rejectError = error ?? new KadiError('Connection closed', 'STDIO_ERROR');
|
|
221
|
-
for (const waiter of this.waiters) {
|
|
222
|
-
clearTimeout(waiter.timeout);
|
|
223
|
-
waiter.reject(rejectError);
|
|
224
|
-
}
|
|
225
|
-
this.waiters = [];
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
230
|
-
// MESSAGE WRITER (Content-Length framing)
|
|
231
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Writes JSON-RPC messages to a stream using Content-Length framing.
|
|
235
|
-
*/
|
|
236
|
-
class StdioWriter {
|
|
237
|
-
constructor(private stream: Writable) {}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Write a message to the stream.
|
|
241
|
-
*/
|
|
242
|
-
write(message: JsonRpcRequest): void {
|
|
243
|
-
const json = JSON.stringify(message);
|
|
244
|
-
const contentLength = Buffer.byteLength(json, 'utf-8');
|
|
245
|
-
const header = `Content-Length: ${contentLength}\r\n\r\n`;
|
|
246
|
-
|
|
247
|
-
this.stream.write(header + json);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
34
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
252
35
|
// STDIO TRANSPORT
|
|
253
36
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -319,8 +102,8 @@ export async function loadStdioTransport(
|
|
|
319
102
|
);
|
|
320
103
|
}
|
|
321
104
|
|
|
322
|
-
const reader = new
|
|
323
|
-
const writer = new
|
|
105
|
+
const reader = new StdioMessageReader(proc.stdout, timeoutMs);
|
|
106
|
+
const writer = new StdioMessageWriter(proc.stdin);
|
|
324
107
|
let idCounter = 1;
|
|
325
108
|
let isShutdown = false;
|
|
326
109
|
|