@love-moon/conductor-sdk 0.1.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 (41) hide show
  1. package/dist/backend/client.d.ts +62 -0
  2. package/dist/backend/client.js +207 -0
  3. package/dist/backend/index.d.ts +1 -0
  4. package/dist/backend/index.js +1 -0
  5. package/dist/bin/mcp-server.d.ts +2 -0
  6. package/dist/bin/mcp-server.js +175 -0
  7. package/dist/config/index.d.ts +33 -0
  8. package/dist/config/index.js +152 -0
  9. package/dist/context/index.d.ts +1 -0
  10. package/dist/context/index.js +1 -0
  11. package/dist/context/project_context.d.ts +14 -0
  12. package/dist/context/project_context.js +92 -0
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.js +9 -0
  15. package/dist/mcp/index.d.ts +2 -0
  16. package/dist/mcp/index.js +2 -0
  17. package/dist/mcp/notifications.d.ts +20 -0
  18. package/dist/mcp/notifications.js +44 -0
  19. package/dist/mcp/server.d.ts +37 -0
  20. package/dist/mcp/server.js +211 -0
  21. package/dist/message/index.d.ts +1 -0
  22. package/dist/message/index.js +1 -0
  23. package/dist/message/router.d.ts +19 -0
  24. package/dist/message/router.js +122 -0
  25. package/dist/orchestrator.d.ts +21 -0
  26. package/dist/orchestrator.js +20 -0
  27. package/dist/reporter/event_stream.d.ts +7 -0
  28. package/dist/reporter/event_stream.js +20 -0
  29. package/dist/reporter/index.d.ts +1 -0
  30. package/dist/reporter/index.js +1 -0
  31. package/dist/session/index.d.ts +2 -0
  32. package/dist/session/index.js +2 -0
  33. package/dist/session/manager.d.ts +39 -0
  34. package/dist/session/manager.js +162 -0
  35. package/dist/session/store.d.ts +36 -0
  36. package/dist/session/store.js +147 -0
  37. package/dist/ws/client.d.ts +49 -0
  38. package/dist/ws/client.js +296 -0
  39. package/dist/ws/index.d.ts +1 -0
  40. package/dist/ws/index.js +1 -0
  41. package/package.json +31 -0
@@ -0,0 +1,296 @@
1
+ class AsyncLock {
2
+ tail = Promise.resolve();
3
+ async runExclusive(fn) {
4
+ const run = this.tail.then(fn, fn);
5
+ this.tail = run
6
+ .then(() => undefined)
7
+ .catch(() => undefined);
8
+ return run;
9
+ }
10
+ }
11
+ export class ConductorWebSocketClient {
12
+ url;
13
+ token;
14
+ reconnectDelay;
15
+ heartbeatInterval;
16
+ connectImpl;
17
+ handlers = [];
18
+ extraHeaders;
19
+ conn = null;
20
+ stop = false;
21
+ listenTask = null;
22
+ heartbeatTask = null;
23
+ lock = new AsyncLock();
24
+ waitController = new AbortController();
25
+ constructor(config, options = {}) {
26
+ this.url = config.resolvedWebsocketUrl;
27
+ this.token = config.agentToken;
28
+ this.reconnectDelay = options.reconnectDelay ?? 3000;
29
+ this.heartbeatInterval = options.heartbeatInterval ?? 20_000;
30
+ this.extraHeaders = {
31
+ 'x-conductor-host': options.hostName ?? defaultHostName(),
32
+ ...(options.extraHeaders ?? {}),
33
+ };
34
+ this.connectImpl = options.connectImpl ?? defaultConnectImpl;
35
+ }
36
+ registerHandler(handler) {
37
+ this.handlers.push(handler);
38
+ }
39
+ async connect() {
40
+ this.stop = false;
41
+ if (this.waitController.signal.aborted) {
42
+ this.waitController = new AbortController();
43
+ }
44
+ await this.openConnection(true);
45
+ }
46
+ async disconnect() {
47
+ this.stop = true;
48
+ this.waitController.abort();
49
+ if (this.listenTask) {
50
+ this.listenTask = null;
51
+ }
52
+ if (this.heartbeatTask) {
53
+ this.heartbeatTask = null;
54
+ }
55
+ if (this.conn && !this.isConnectionClosed(this.conn)) {
56
+ await this.conn.close();
57
+ }
58
+ this.conn = null;
59
+ }
60
+ async sendJson(payload) {
61
+ await this.ensureConnection();
62
+ await this.sendWithReconnect(JSON.stringify(payload));
63
+ }
64
+ async ensureConnection() {
65
+ if (this.conn && !this.isConnectionClosed(this.conn)) {
66
+ return;
67
+ }
68
+ await this.openConnection(true);
69
+ }
70
+ async openConnection(force = false) {
71
+ await this.lock.runExclusive(async () => {
72
+ if (this.conn && !this.isConnectionClosed(this.conn) && !force) {
73
+ return;
74
+ }
75
+ await this.cancelTasks();
76
+ while (!this.stop) {
77
+ try {
78
+ const headers = { Authorization: `Bearer ${this.token}`, ...this.extraHeaders };
79
+ this.conn = await this.connectImpl(this.url, { headers });
80
+ this.listenTask = this.listenLoop(this.conn);
81
+ this.heartbeatTask = this.heartbeatLoop(this.conn);
82
+ return;
83
+ }
84
+ catch (error) {
85
+ if (force) {
86
+ console.warn(`[WebSocket] Connection failed, retrying in ${this.reconnectDelay}ms... (${error instanceof Error ? error.message : String(error)})`);
87
+ }
88
+ await wait(this.reconnectDelay, this.waitController.signal);
89
+ }
90
+ }
91
+ });
92
+ }
93
+ async cancelTasks() {
94
+ this.listenTask = null;
95
+ this.heartbeatTask = null;
96
+ }
97
+ async listenLoop(conn) {
98
+ try {
99
+ for await (const message of conn) {
100
+ await this.dispatch(message);
101
+ }
102
+ }
103
+ catch {
104
+ // Ignore errors; reconnection logic handles it.
105
+ }
106
+ finally {
107
+ if (!this.stop && conn === this.conn) {
108
+ await this.openConnection(true);
109
+ }
110
+ }
111
+ }
112
+ async heartbeatLoop(conn) {
113
+ try {
114
+ while (!this.stop && !this.isConnectionClosed(conn)) {
115
+ await wait(this.heartbeatInterval, this.waitController.signal);
116
+ try {
117
+ await conn.ping();
118
+ }
119
+ catch {
120
+ break;
121
+ }
122
+ }
123
+ }
124
+ finally {
125
+ if (!this.stop && conn === this.conn) {
126
+ await this.openConnection(true);
127
+ }
128
+ }
129
+ }
130
+ async dispatch(message) {
131
+ let payload;
132
+ try {
133
+ payload = JSON.parse(message);
134
+ }
135
+ catch {
136
+ return;
137
+ }
138
+ for (const handler of this.handlers) {
139
+ const result = handler(payload);
140
+ if (result && typeof result.then === 'function') {
141
+ await result;
142
+ }
143
+ }
144
+ }
145
+ isConnectionClosed(conn) {
146
+ if (!conn) {
147
+ return true;
148
+ }
149
+ if (typeof conn.closed === 'boolean') {
150
+ return conn.closed;
151
+ }
152
+ return false;
153
+ }
154
+ async sendWithReconnect(data) {
155
+ let attemptedReconnect = false;
156
+ // Loop at most twice: initial send, then one reconnect + retry
157
+ while (true) {
158
+ const conn = this.conn;
159
+ if (!conn || this.isConnectionClosed(conn)) {
160
+ await this.openConnection(true);
161
+ }
162
+ if (!this.conn || this.isConnectionClosed(this.conn)) {
163
+ throw new Error('WebSocket not connected');
164
+ }
165
+ try {
166
+ await this.conn.send(data);
167
+ return;
168
+ }
169
+ catch (error) {
170
+ if (attemptedReconnect || !this.isNotOpenError(error)) {
171
+ throw error instanceof Error ? error : new Error(String(error));
172
+ }
173
+ attemptedReconnect = true;
174
+ await this.openConnection(true);
175
+ }
176
+ }
177
+ }
178
+ isNotOpenError(error) {
179
+ if (!error)
180
+ return false;
181
+ const message = error instanceof Error ? error.message : String(error);
182
+ return message.toLowerCase().includes('websocket is not open');
183
+ }
184
+ }
185
+ async function wait(ms, signal) {
186
+ if (!signal) {
187
+ await new Promise((resolve) => setTimeout(resolve, ms));
188
+ return;
189
+ }
190
+ if (signal.aborted) {
191
+ return;
192
+ }
193
+ await new Promise((resolve) => {
194
+ const timer = setTimeout(resolve, ms);
195
+ const onAbort = () => {
196
+ clearTimeout(timer);
197
+ resolve();
198
+ };
199
+ signal.addEventListener('abort', onAbort, { once: true });
200
+ });
201
+ }
202
+ async function defaultConnectImpl(url, options) {
203
+ const { default: WebSocket } = await import('ws');
204
+ return new Promise((resolve, reject) => {
205
+ const ws = new WebSocket(url, {
206
+ headers: options.headers,
207
+ perMessageDeflate: false,
208
+ });
209
+ ws.once('open', () => resolve(new WsAdapter(ws)));
210
+ ws.once('error', (err) => reject(err));
211
+ });
212
+ }
213
+ function defaultHostName() {
214
+ const pid = process.pid;
215
+ const host = process.env.HOSTNAME || process.env.COMPUTERNAME || 'unknown-host';
216
+ return `conductor-fire-${host}-${pid}`;
217
+ }
218
+ class WsAdapter {
219
+ ws;
220
+ queue = [];
221
+ waiters = [];
222
+ closed = false;
223
+ constructor(ws) {
224
+ this.ws = ws;
225
+ ws.on('message', (data) => this.enqueue(data.toString()));
226
+ ws.on('close', () => {
227
+ this.closed = true;
228
+ this.enqueue(null);
229
+ });
230
+ ws.on('error', () => {
231
+ this.closed = true;
232
+ this.enqueue(null);
233
+ });
234
+ }
235
+ send(data) {
236
+ return new Promise((resolve, reject) => {
237
+ this.ws.send(data, (error) => {
238
+ if (error) {
239
+ reject(error);
240
+ }
241
+ else {
242
+ resolve();
243
+ }
244
+ });
245
+ });
246
+ }
247
+ ping() {
248
+ return new Promise((resolve, reject) => {
249
+ this.ws.ping(undefined, undefined, (error) => {
250
+ if (error) {
251
+ reject(error);
252
+ }
253
+ else {
254
+ resolve();
255
+ }
256
+ });
257
+ });
258
+ }
259
+ close() {
260
+ return new Promise((resolve) => {
261
+ if (this.closed) {
262
+ resolve();
263
+ return;
264
+ }
265
+ this.ws.once('close', () => resolve());
266
+ this.ws.close();
267
+ });
268
+ }
269
+ async *[Symbol.asyncIterator]() {
270
+ while (true) {
271
+ const value = await this.nextValue();
272
+ if (value === null) {
273
+ return;
274
+ }
275
+ yield value;
276
+ }
277
+ }
278
+ nextValue() {
279
+ if (this.queue.length) {
280
+ const value = this.queue.shift() ?? null;
281
+ return Promise.resolve(value);
282
+ }
283
+ return new Promise((resolve) => {
284
+ this.waiters.push(resolve);
285
+ });
286
+ }
287
+ enqueue(value) {
288
+ if (this.waiters.length) {
289
+ const resolve = this.waiters.shift();
290
+ resolve(value);
291
+ }
292
+ else {
293
+ this.queue.push(value);
294
+ }
295
+ }
296
+ }
@@ -0,0 +1 @@
1
+ export * from './client.js';
@@ -0,0 +1 @@
1
+ export * from './client.js';
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@love-moon/conductor-sdk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.json",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.23.0",
21
+ "ws": "^8.18.0",
22
+ "yaml": "^2.6.0",
23
+ "zod": "^3.24.1"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.10.2",
27
+ "@types/ws": "^8.5.12",
28
+ "typescript": "^5.6.3",
29
+ "vitest": "^2.1.4"
30
+ }
31
+ }