@thinkwell/conductor 0.5.5 → 0.5.6

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.
@@ -1,155 +1,88 @@
1
- /**
2
- * ChannelConnector - in-memory bidirectional connection
3
- *
4
- * This connector creates an in-memory channel for testing or for
5
- * embedded components that run in the same process.
6
- */
7
- /**
8
- * A simple message channel that supports async iteration
9
- */
10
1
  class MessageChannel {
11
- queue = [];
12
- resolvers = [];
13
- closed = false;
14
- push(message) {
15
- if (this.closed) {
16
- return;
17
- }
18
- if (this.resolvers.length > 0) {
19
- const resolve = this.resolvers.shift();
20
- resolve(message);
21
- }
22
- else {
23
- this.queue.push(message);
24
- }
25
- }
26
- close() {
27
- this.closed = true;
28
- for (const resolve of this.resolvers) {
29
- resolve(null);
30
- }
31
- this.resolvers = [];
32
- }
33
- isClosed() {
34
- return this.closed;
35
- }
36
- async *[Symbol.asyncIterator]() {
37
- while (!this.closed) {
38
- let message;
39
- if (this.queue.length > 0) {
40
- message = this.queue.shift();
41
- }
42
- else {
43
- message = await new Promise((resolve) => {
44
- this.resolvers.push(resolve);
45
- });
46
- }
47
- if (message === null) {
48
- return;
49
- }
50
- yield message;
51
- }
52
- // Drain any remaining messages
53
- while (this.queue.length > 0) {
54
- yield this.queue.shift();
55
- }
2
+ queue = [];
3
+ resolvers = [];
4
+ closed = !1;
5
+ push(message) {
6
+ this.closed || (this.resolvers.length > 0 ? this.resolvers.shift()(message) : this.queue.push(message));
7
+ }
8
+ close() {
9
+ this.closed = !0;
10
+ for (const resolve of this.resolvers)
11
+ resolve(null);
12
+ this.resolvers = [];
13
+ }
14
+ isClosed() {
15
+ return this.closed;
16
+ }
17
+ async *[Symbol.asyncIterator]() {
18
+ for (; !this.closed; ) {
19
+ let message;
20
+ if (this.queue.length > 0 ? message = this.queue.shift() : message = await new Promise((resolve) => {
21
+ this.resolvers.push(resolve);
22
+ }), message === null)
23
+ return;
24
+ yield message;
56
25
  }
26
+ for (; this.queue.length > 0; )
27
+ yield this.queue.shift();
28
+ }
57
29
  }
58
- /**
59
- * One side of a channel connection
60
- */
61
30
  class ChannelConnectionEnd {
62
- sendChannel;
63
- receiveChannel;
64
- constructor(sendChannel, receiveChannel) {
65
- this.sendChannel = sendChannel;
66
- this.receiveChannel = receiveChannel;
67
- }
68
- send(message) {
69
- this.sendChannel.push(message);
70
- }
71
- get messages() {
72
- return this.receiveChannel;
73
- }
74
- async close() {
75
- this.sendChannel.close();
76
- this.receiveChannel.close();
77
- }
31
+ sendChannel;
32
+ receiveChannel;
33
+ constructor(sendChannel, receiveChannel) {
34
+ this.sendChannel = sendChannel, this.receiveChannel = receiveChannel;
35
+ }
36
+ send(message) {
37
+ this.sendChannel.push(message);
38
+ }
39
+ get messages() {
40
+ return this.receiveChannel;
41
+ }
42
+ async close() {
43
+ this.sendChannel.close(), this.receiveChannel.close();
44
+ }
78
45
  }
79
- /**
80
- * Create a connected pair of channel endpoints.
81
- *
82
- * This is useful for testing or for connecting in-process components.
83
- */
84
46
  export function createChannelPair() {
85
- const leftToRight = new MessageChannel();
86
- const rightToLeft = new MessageChannel();
87
- return {
88
- left: new ChannelConnectionEnd(leftToRight, rightToLeft),
89
- right: new ChannelConnectionEnd(rightToLeft, leftToRight),
90
- };
47
+ const leftToRight = new MessageChannel(), rightToLeft = new MessageChannel();
48
+ return {
49
+ left: new ChannelConnectionEnd(leftToRight, rightToLeft),
50
+ right: new ChannelConnectionEnd(rightToLeft, leftToRight)
51
+ };
91
52
  }
92
- /**
93
- * Connector for in-memory channel connections.
94
- *
95
- * Each call to connect() returns a new channel pair. The "other" end
96
- * must be retrieved via getOtherEnd() after calling connect().
97
- */
98
53
  export class ChannelConnector {
99
- pendingOtherEnd = null;
100
- async connect() {
101
- const pair = createChannelPair();
102
- this.pendingOtherEnd = pair.right;
103
- return pair.left;
104
- }
105
- /**
106
- * Get the other end of the most recently created channel.
107
- *
108
- * This must be called after connect() to get the paired endpoint.
109
- */
110
- getOtherEnd() {
111
- const end = this.pendingOtherEnd;
112
- this.pendingOtherEnd = null;
113
- return end;
114
- }
54
+ pendingOtherEnd = null;
55
+ async connect() {
56
+ const pair = createChannelPair();
57
+ return this.pendingOtherEnd = pair.right, pair.left;
58
+ }
59
+ /**
60
+ * Get the other end of the most recently created channel.
61
+ *
62
+ * This must be called after connect() to get the paired endpoint.
63
+ */
64
+ getOtherEnd() {
65
+ const end = this.pendingOtherEnd;
66
+ return this.pendingOtherEnd = null, end;
67
+ }
115
68
  }
116
- /**
117
- * Create a connector that runs a handler function in-process.
118
- *
119
- * This is useful for testing or for embedding simple components.
120
- *
121
- * @param handler - Function that handles the component side of the connection
122
- * @returns A connector that runs the handler when connect() is called
123
- */
124
69
  export function inProcess(handler) {
125
- return {
126
- async connect() {
127
- const pair = createChannelPair();
128
- // Start the handler on the other end (don't await it)
129
- handler(pair.right).catch((error) => {
130
- console.error("In-process component error:", error);
131
- });
132
- return pair.left;
133
- },
134
- };
70
+ return {
71
+ async connect() {
72
+ const pair = createChannelPair();
73
+ return handler(pair.right).catch((error) => {
74
+ console.error("In-process component error:", error);
75
+ }), pair.left;
76
+ }
77
+ };
135
78
  }
136
- /**
137
- * Create a simple echo component for testing.
138
- *
139
- * This component echoes back any request with the same params as the result.
140
- */
141
79
  export function echoComponent() {
142
- return inProcess(async (connection) => {
143
- for await (const message of connection.messages) {
144
- if ("method" in message && "id" in message) {
145
- // It's a request - echo the params back as the result
146
- connection.send({
147
- jsonrpc: "2.0",
148
- id: message.id,
149
- result: message.params,
150
- });
151
- }
152
- }
153
- });
80
+ return inProcess(async (connection) => {
81
+ for await (const message of connection.messages)
82
+ "method" in message && "id" in message && connection.send({
83
+ jsonrpc: "2.0",
84
+ id: message.id,
85
+ result: message.params
86
+ });
87
+ });
154
88
  }
155
- //# sourceMappingURL=channel.js.map
@@ -1,6 +1,2 @@
1
- /**
2
- * Component connectors - factories for creating component connections
3
- */
4
1
  export { StdioConnector, stdio } from "./stdio.js";
5
- export { ChannelConnector, createChannelPair, inProcess, echoComponent, } from "./channel.js";
6
- //# sourceMappingURL=index.js.map
2
+ export { ChannelConnector, createChannelPair, inProcess, echoComponent } from "./channel.js";
@@ -1,214 +1,112 @@
1
- /**
2
- * StdioConnector - spawns a subprocess and communicates via stdin/stdout
3
- *
4
- * This connector starts a child process and establishes a bidirectional
5
- * JSON-RPC connection using newline-delimited JSON over stdio.
6
- */
7
1
  import { spawn } from "node:child_process";
8
2
  import { createInterface } from "node:readline";
9
- /**
10
- * A connection to a subprocess via stdin/stdout
11
- */
12
3
  class StdioConnection {
13
- process;
14
- readline;
15
- closed = false;
16
- closingGracefully = false;
17
- constructor(process) {
18
- this.process = process;
19
- if (!process.stdout || !process.stdin) {
20
- throw new Error("Process must have stdio pipes");
21
- }
22
- // Set up readline for reading newline-delimited JSON
23
- this.readline = createInterface({
24
- input: process.stdout,
25
- crlfDelay: Infinity,
26
- });
27
- // Handle process exit
28
- process.on("exit", (code, signal) => {
29
- this.closed = true;
30
- // Don't log exit codes during graceful shutdown - we expect non-zero codes
31
- // when we send SIGTERM/SIGKILL to processes that don't handle stdin EOF.
32
- // This is a workaround for kiro-cli acp (v1.25.0) which doesn't exit on stdin close.
33
- if (code !== 0 && code !== null && !this.closingGracefully) {
34
- console.error(`Process exited with code ${code}`);
35
- }
36
- // Only log signal if it wasn't an expected graceful shutdown
37
- if (signal && !this.closingGracefully) {
38
- console.error(`Process killed by signal ${signal}`);
39
- }
40
- });
41
- process.on("error", (error) => {
42
- console.error("Process error:", error);
43
- this.closed = true;
44
- });
45
- }
46
- /**
47
- * Send a JSON-RPC message to the subprocess
48
- */
49
- send(message) {
50
- if (this.closed || !this.process.stdin) {
51
- throw new Error("Connection is closed");
52
- }
53
- const json = JSON.stringify(message);
54
- this.process.stdin.write(json + "\n");
55
- }
56
- /**
57
- * Async iterable of messages received from the subprocess
58
- */
59
- get messages() {
60
- const readline = this.readline;
61
- const isClosed = () => this.closed;
62
- return {
63
- async *[Symbol.asyncIterator]() {
64
- for await (const line of readline) {
65
- if (isClosed()) {
66
- return;
67
- }
68
- if (!line.trim()) {
69
- continue;
70
- }
71
- try {
72
- const message = JSON.parse(line);
73
- yield message;
74
- }
75
- catch (error) {
76
- console.error("Failed to parse JSON-RPC message:", error);
77
- console.error("Line:", line);
78
- }
79
- }
80
- },
81
- };
82
- }
83
- /**
84
- * Close the connection and terminate the subprocess
85
- */
86
- async close() {
87
- if (this.closed) {
4
+ process;
5
+ readline;
6
+ closed = !1;
7
+ closingGracefully = !1;
8
+ constructor(process) {
9
+ if (this.process = process, !process.stdout || !process.stdin)
10
+ throw new Error("Process must have stdio pipes");
11
+ this.readline = createInterface({
12
+ input: process.stdout,
13
+ crlfDelay: 1 / 0
14
+ }), process.on("exit", (code, signal) => {
15
+ this.closed = !0, code !== 0 && code !== null && !this.closingGracefully && console.error(`Process exited with code ${code}`), signal && !this.closingGracefully && console.error(`Process killed by signal ${signal}`);
16
+ }), process.on("error", (error) => {
17
+ console.error("Process error:", error), this.closed = !0;
18
+ });
19
+ }
20
+ /**
21
+ * Send a JSON-RPC message to the subprocess
22
+ */
23
+ send(message) {
24
+ if (this.closed || !this.process.stdin)
25
+ throw new Error("Connection is closed");
26
+ const json = JSON.stringify(message);
27
+ this.process.stdin.write(json + `
28
+ `);
29
+ }
30
+ /**
31
+ * Async iterable of messages received from the subprocess
32
+ */
33
+ get messages() {
34
+ const readline = this.readline, isClosed = () => this.closed;
35
+ return {
36
+ async *[Symbol.asyncIterator]() {
37
+ for await (const line of readline) {
38
+ if (isClosed())
88
39
  return;
40
+ if (line.trim())
41
+ try {
42
+ yield JSON.parse(line);
43
+ } catch (error) {
44
+ console.error("Failed to parse JSON-RPC message:", error), console.error("Line:", line);
45
+ }
89
46
  }
90
- this.closed = true;
91
- this.closingGracefully = true;
92
- // Close the readline interface
93
- this.readline.close();
94
- // Close the stdout stream to release the pipe handle
95
- this.process.stdout?.destroy();
96
- // Close stdin to signal EOF to the subprocess
97
- this.process.stdin?.end();
98
- // Wait for the process to exit gracefully, with fallback to signals
99
- // Note: Some processes don't properly handle stdin EOF and need explicit signals.
100
- // For example, kiro-cli acp (as of v1.25.0) doesn't exit on stdin close.
101
- await new Promise((resolve) => {
102
- let sigtermTimeout = null;
103
- let sigkillTimeout = null;
104
- const onExit = () => {
105
- if (sigtermTimeout)
106
- clearTimeout(sigtermTimeout);
107
- if (sigkillTimeout)
108
- clearTimeout(sigkillTimeout);
109
- resolve();
110
- };
111
- this.process.once("exit", onExit);
112
- // Wait 250ms for graceful exit, then send SIGTERM
113
- sigtermTimeout = setTimeout(() => {
114
- this.process.kill("SIGTERM");
115
- // Wait another 500ms, then force kill with SIGKILL
116
- sigkillTimeout = setTimeout(() => {
117
- this.process.kill("SIGKILL");
118
- resolve();
119
- }, 500);
120
- }, 250);
121
- });
122
- }
47
+ }
48
+ };
49
+ }
50
+ /**
51
+ * Close the connection and terminate the subprocess
52
+ */
53
+ async close() {
54
+ this.closed || (this.closed = !0, this.closingGracefully = !0, this.readline.close(), this.process.stdout?.destroy(), this.process.stdin?.end(), await new Promise((resolve) => {
55
+ let sigtermTimeout = null, sigkillTimeout = null;
56
+ const onExit = () => {
57
+ sigtermTimeout && clearTimeout(sigtermTimeout), sigkillTimeout && clearTimeout(sigkillTimeout), resolve();
58
+ };
59
+ this.process.once("exit", onExit), sigtermTimeout = setTimeout(() => {
60
+ this.process.kill("SIGTERM"), sigkillTimeout = setTimeout(() => {
61
+ this.process.kill("SIGKILL"), resolve();
62
+ }, 500);
63
+ }, 250);
64
+ }));
65
+ }
123
66
  }
124
- /**
125
- * Connector that spawns a subprocess and communicates via stdio
126
- */
127
67
  export class StdioConnector {
128
- options;
129
- constructor(options) {
130
- if (typeof options === "string") {
131
- // Parse command string (split on whitespace, respecting quotes)
132
- const parts = parseCommand(options);
133
- this.options = {
134
- command: parts[0],
135
- args: parts.slice(1),
136
- };
137
- }
138
- else {
139
- this.options = options;
140
- }
141
- }
142
- /**
143
- * Spawn the subprocess and return a connection to it
144
- */
145
- async connect() {
146
- const { command, args = [], env, cwd } = this.options;
147
- const process = spawn(command, args, {
148
- stdio: ["pipe", "pipe", "inherit"], // stdin, stdout piped; stderr inherited
149
- env: env ? { ...globalThis.process.env, ...env } : undefined,
150
- cwd,
151
- });
152
- // Wait for the process to spawn successfully
153
- await new Promise((resolve, reject) => {
154
- const onSpawn = () => {
155
- cleanup();
156
- resolve();
157
- };
158
- const onError = (error) => {
159
- cleanup();
160
- reject(error);
161
- };
162
- const cleanup = () => {
163
- process.removeListener("spawn", onSpawn);
164
- process.removeListener("error", onError);
165
- };
166
- process.once("spawn", onSpawn);
167
- process.once("error", onError);
168
- });
169
- return new StdioConnection(process);
170
- }
68
+ options;
69
+ constructor(options) {
70
+ if (typeof options == "string") {
71
+ const parts = parseCommand(options);
72
+ this.options = {
73
+ command: parts[0],
74
+ args: parts.slice(1)
75
+ };
76
+ } else
77
+ this.options = options;
78
+ }
79
+ /**
80
+ * Spawn the subprocess and return a connection to it
81
+ */
82
+ async connect() {
83
+ const { command, args = [], env, cwd } = this.options, process = spawn(command, args, {
84
+ stdio: ["pipe", "pipe", "inherit"],
85
+ // stdin, stdout piped; stderr inherited
86
+ env: env ? { ...globalThis.process.env, ...env } : void 0,
87
+ cwd
88
+ });
89
+ return await new Promise((resolve, reject) => {
90
+ const onSpawn = () => {
91
+ cleanup(), resolve();
92
+ }, onError = (error) => {
93
+ cleanup(), reject(error);
94
+ }, cleanup = () => {
95
+ process.removeListener("spawn", onSpawn), process.removeListener("error", onError);
96
+ };
97
+ process.once("spawn", onSpawn), process.once("error", onError);
98
+ }), new StdioConnection(process);
99
+ }
171
100
  }
172
- /**
173
- * Parse a command string into command and arguments.
174
- * Handles quoted strings.
175
- */
176
101
  function parseCommand(command) {
177
- const parts = [];
178
- let current = "";
179
- let inQuote = null;
180
- for (let i = 0; i < command.length; i++) {
181
- const char = command[i];
182
- if (inQuote) {
183
- if (char === inQuote) {
184
- inQuote = null;
185
- }
186
- else {
187
- current += char;
188
- }
189
- }
190
- else if (char === '"' || char === "'") {
191
- inQuote = char;
192
- }
193
- else if (char === " " || char === "\t") {
194
- if (current) {
195
- parts.push(current);
196
- current = "";
197
- }
198
- }
199
- else {
200
- current += char;
201
- }
202
- }
203
- if (current) {
204
- parts.push(current);
205
- }
206
- return parts;
102
+ const parts = [];
103
+ let current = "", inQuote = null;
104
+ for (let i = 0; i < command.length; i++) {
105
+ const char = command[i];
106
+ inQuote ? char === inQuote ? inQuote = null : current += char : char === '"' || char === "'" ? inQuote = char : char === " " || char === " " ? current && (parts.push(current), current = "") : current += char;
107
+ }
108
+ return current && parts.push(current), parts;
207
109
  }
208
- /**
209
- * Create a StdioConnector from a command string or options
210
- */
211
110
  export function stdio(options) {
212
- return new StdioConnector(options);
111
+ return new StdioConnector(options);
213
112
  }
214
- //# sourceMappingURL=stdio.js.map
@@ -0,0 +1,5 @@
1
+ export declare const features: {
2
+ readonly LOG_PERMISSIONS: false;
3
+ readonly STRIP_CLAUDECODE_ENV: false;
4
+ };
5
+ //# sourceMappingURL=features.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../../src/generated/features.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,QAAQ;;;CAGX,CAAC"}
@@ -0,0 +1,4 @@
1
+ export const features = {
2
+ LOG_PERMISSIONS: !1,
3
+ STRIP_CLAUDECODE_ENV: !1
4
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features.js","sourceRoot":"","sources":["../../src/generated/features.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,eAAe,EAAE,KAAK;IACtB,oBAAoB,EAAE,KAAK;CACnB,CAAC"}
package/dist/index.js CHANGED
@@ -1,76 +1,8 @@
1
- /**
2
- * @thinkwell/conductor - TypeScript conductor for ACP proxy chains
3
- *
4
- * The conductor orchestrates message routing between clients, proxies, and agents.
5
- * It sits between every component, managing process lifecycle and message flow.
6
- *
7
- * ## Quick Start
8
- *
9
- * ```typescript
10
- * import { Conductor, fromCommands, createChannelPair } from '@thinkwell/conductor';
11
- *
12
- * // Create a conductor that spawns an agent subprocess
13
- * const conductor = new Conductor({
14
- * instantiator: fromCommands(['my-agent']),
15
- * });
16
- *
17
- * // Connect via a channel (for testing) or stdio (for production)
18
- * const [clientEnd, conductorEnd] = createChannelPair();
19
- * await conductor.connect(conductorEnd);
20
- * ```
21
- *
22
- * ## Logging
23
- *
24
- * Enable logging to see what the conductor is doing:
25
- *
26
- * ```typescript
27
- * const conductor = new Conductor({
28
- * instantiator: fromCommands(['my-agent']),
29
- * logging: {
30
- * level: 'debug', // 'error' | 'warn' | 'info' | 'debug' | 'trace'
31
- * name: 'my-app',
32
- * },
33
- * });
34
- * ```
35
- *
36
- * ## JSONL Tracing
37
- *
38
- * Write all messages to a JSONL file for debugging:
39
- *
40
- * ```typescript
41
- * const conductor = new Conductor({
42
- * instantiator: fromCommands(['my-agent']),
43
- * trace: {
44
- * path: '/tmp/conductor-trace.jsonl',
45
- * },
46
- * });
47
- * ```
48
- *
49
- * ## Architecture
50
- *
51
- * The conductor uses a central message queue to preserve ordering:
52
- *
53
- * ```
54
- * Client ←→ Conductor ←→ [Proxy 0] ←→ [Proxy 1] ←→ ... ←→ Agent
55
- * ```
56
- *
57
- * All messages flow through the conductor's event loop, ensuring that
58
- * responses never overtake notifications.
59
- *
60
- * @module
61
- */
62
- // Conductor
63
1
  export { Conductor } from "./conductor.js";
64
- // Logging
65
- export { createLogger, createNoopLogger, getLogger, setLogger, } from "./logger.js";
66
- // Instantiators
67
- export { fromCommands, fromConnectors, dynamic, staticInstantiator, } from "./instantiators.js";
2
+ export { createLogger, createNoopLogger, getLogger, setLogger } from "./logger.js";
3
+ export { fromCommands, fromConnectors, dynamic, staticInstantiator } from "./instantiators.js";
68
4
  export { ROLE_COUNTERPART } from "./types.js";
69
- // Message queue
70
5
  export { MessageQueue } from "./message-queue.js";
71
- // Connectors
72
- export { StdioConnector, stdio, ChannelConnector, createChannelPair, inProcess, echoComponent, } from "./connectors/index.js";
73
- // MCP Bridge
74
- export { McpBridge, createHttpListener, } from "./mcp-bridge/index.js";
75
- export { isJsonRpcRequest, isJsonRpcNotification, isJsonRpcResponse, createRequest, createNotification, createSuccessResponse, createErrorResponse, createResponder, } from "@thinkwell/protocol";
76
- //# sourceMappingURL=index.js.map
6
+ export { StdioConnector, stdio, ChannelConnector, createChannelPair, inProcess, echoComponent } from "./connectors/index.js";
7
+ export { McpBridge, createHttpListener } from "./mcp-bridge/index.js";
8
+ export { isJsonRpcRequest, isJsonRpcNotification, isJsonRpcResponse, createRequest, createNotification, createSuccessResponse, createErrorResponse, createResponder } from "@thinkwell/protocol";