@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.
Files changed (45) hide show
  1. package/README.md +424 -1
  2. package/agent.json +19 -0
  3. package/dist/agent-json.d.ts +231 -0
  4. package/dist/agent-json.d.ts.map +1 -0
  5. package/dist/agent-json.js +554 -0
  6. package/dist/agent-json.js.map +1 -0
  7. package/dist/client.d.ts +34 -0
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +50 -0
  10. package/dist/client.js.map +1 -1
  11. package/dist/errors.d.ts +1 -1
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js.map +1 -1
  14. package/dist/index.d.ts +5 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +8 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/process-manager.d.ts +235 -0
  19. package/dist/process-manager.d.ts.map +1 -0
  20. package/dist/process-manager.js +647 -0
  21. package/dist/process-manager.js.map +1 -0
  22. package/dist/stdio-framing.d.ts +88 -0
  23. package/dist/stdio-framing.d.ts.map +1 -0
  24. package/dist/stdio-framing.js +194 -0
  25. package/dist/stdio-framing.js.map +1 -0
  26. package/dist/transports/stdio.d.ts.map +1 -1
  27. package/dist/transports/stdio.js +3 -181
  28. package/dist/transports/stdio.js.map +1 -1
  29. package/dist/types.d.ts +256 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/utils.d.ts +107 -0
  32. package/dist/utils.d.ts.map +1 -0
  33. package/dist/utils.js +212 -0
  34. package/dist/utils.js.map +1 -0
  35. package/package.json +3 -1
  36. package/scripts/symlink.mjs +131 -0
  37. package/src/agent-json.ts +655 -0
  38. package/src/client.ts +56 -0
  39. package/src/errors.ts +15 -0
  40. package/src/index.ts +32 -0
  41. package/src/process-manager.ts +821 -0
  42. package/src/stdio-framing.ts +227 -0
  43. package/src/transports/stdio.ts +4 -221
  44. package/src/types.ts +277 -0
  45. 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
+ }
@@ -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, JsonRpcRequest, JsonRpcResponse, EventHandler } from '../types.js';
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 StdioReader(proc.stdout, timeoutMs);
323
- const writer = new StdioWriter(proc.stdin);
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