@love-moon/conductor-sdk 0.2.5 → 0.2.7

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.
@@ -75,6 +75,10 @@ export declare class BackendApiClient {
75
75
  metadata?: Record<string, unknown>;
76
76
  }): Promise<ProjectSummary>;
77
77
  private request;
78
+ private buildUrl;
79
+ private sendRequest;
80
+ private shouldRetryWithApiPrefix;
81
+ private withApiPrefix;
78
82
  private parseJson;
79
83
  private safeJson;
80
84
  }
@@ -167,23 +167,16 @@ export class BackendApiClient {
167
167
  return ProjectSummary.fromJSON(payload);
168
168
  }
169
169
  async request(method, pathname, opts = {}) {
170
- const url = new URL(`${this.baseUrl}${pathname}`);
171
- if (opts.query) {
172
- opts.query.forEach((value, key) => url.searchParams.set(key, value));
173
- }
170
+ const url = this.buildUrl(pathname, opts.query);
174
171
  const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
175
172
  const timeout = controller ? setTimeout(() => controller.abort(), this.timeoutMs) : null;
176
173
  try {
177
- const response = await this.fetchImpl(url.toString(), {
178
- method,
179
- headers: {
180
- Authorization: `Bearer ${this.config.agentToken}`,
181
- Accept: 'application/json',
182
- ...(opts.body ? { 'Content-Type': 'application/json' } : {}),
183
- },
184
- body: opts.body,
185
- signal: controller?.signal,
186
- });
174
+ let response = await this.sendRequest(url, method, opts.body, controller);
175
+ if (response.status === 404 &&
176
+ this.shouldRetryWithApiPrefix(pathname, url)) {
177
+ const retryUrl = this.buildUrl(this.withApiPrefix(pathname), opts.query);
178
+ response = await this.sendRequest(retryUrl, method, opts.body, controller);
179
+ }
187
180
  if (!response.ok) {
188
181
  const details = await this.safeJson(response);
189
182
  throw new BackendApiError(`Backend responded with ${response.status}`, response.status, details);
@@ -202,6 +195,39 @@ export class BackendApiClient {
202
195
  }
203
196
  }
204
197
  }
198
+ buildUrl(pathname, query) {
199
+ const url = new URL(`${this.baseUrl}${pathname}`);
200
+ if (query) {
201
+ query.forEach((value, key) => url.searchParams.set(key, value));
202
+ }
203
+ return url;
204
+ }
205
+ async sendRequest(url, method, body, controller) {
206
+ return this.fetchImpl(url.toString(), {
207
+ method,
208
+ headers: {
209
+ Authorization: `Bearer ${this.config.agentToken}`,
210
+ Accept: 'application/json',
211
+ ...(body ? { 'Content-Type': 'application/json' } : {}),
212
+ },
213
+ body,
214
+ signal: controller?.signal,
215
+ });
216
+ }
217
+ shouldRetryWithApiPrefix(pathname, url) {
218
+ if (!pathname.startsWith('/'))
219
+ return false;
220
+ if (pathname === '/api' || pathname.startsWith('/api/'))
221
+ return false;
222
+ // If backendUrl already includes /api in the base path, don't retry.
223
+ return !url.pathname.startsWith('/api/');
224
+ }
225
+ withApiPrefix(pathname) {
226
+ if (pathname === '/api' || pathname.startsWith('/api/')) {
227
+ return pathname;
228
+ }
229
+ return `/api${pathname}`;
230
+ }
205
231
  async parseJson(response) {
206
232
  try {
207
233
  return await response.json();
@@ -0,0 +1,64 @@
1
+ import { BackendApiClient } from './backend/index.js';
2
+ import { ConductorConfig } from './config/index.js';
3
+ import { MessageRouter } from './message/index.js';
4
+ import { SessionDiskStore, SessionManager } from './session/index.js';
5
+ import { ConductorWebSocketClient } from './ws/index.js';
6
+ type BackendApiLike = Pick<BackendApiClient, 'listProjects' | 'createProject' | 'listTasks' | 'createTask' | 'matchProjectByPath' | 'getProject' | 'updateProject'>;
7
+ type RealtimeClientLike = Pick<ConductorWebSocketClient, 'registerHandler' | 'connect' | 'disconnect' | 'sendJson'>;
8
+ export interface ConductorClientConnectOptions {
9
+ config?: ConductorConfig;
10
+ configFile?: string;
11
+ env?: Record<string, string | undefined>;
12
+ extraEnv?: Record<string, string | undefined>;
13
+ projectPath?: string;
14
+ backendApi?: BackendApiLike;
15
+ wsClient?: RealtimeClientLike;
16
+ sessionManager?: SessionManager;
17
+ sessionStore?: SessionDiskStore;
18
+ messageRouter?: MessageRouter;
19
+ agentHost?: string;
20
+ }
21
+ interface ConductorClientInit {
22
+ config: ConductorConfig;
23
+ env: Record<string, string | undefined>;
24
+ projectPath: string;
25
+ backendApi: BackendApiLike;
26
+ wsClient: RealtimeClientLike;
27
+ sessionManager: SessionManager;
28
+ sessionStore: SessionDiskStore;
29
+ messageRouter: MessageRouter;
30
+ agentHost: string;
31
+ }
32
+ export declare class ConductorClient {
33
+ private readonly config;
34
+ private readonly env;
35
+ private readonly projectPath;
36
+ private readonly backendApi;
37
+ private readonly wsClient;
38
+ private readonly sessions;
39
+ private readonly sessionStore;
40
+ private readonly messageRouter;
41
+ private readonly agentHost;
42
+ private closed;
43
+ constructor(init: ConductorClientInit);
44
+ static connect(options?: ConductorClientConnectOptions): Promise<ConductorClient>;
45
+ close(): Promise<void>;
46
+ createTaskSession(payload: Record<string, any>): Promise<Record<string, any>>;
47
+ sendMessage(taskId: string, content: string, metadata?: Record<string, any>): Promise<Record<string, any>>;
48
+ sendTaskStatus(taskId: string, payload: Record<string, any>): Promise<Record<string, any>>;
49
+ sendRuntimeStatus(taskId: string, payload: Record<string, any>): Promise<Record<string, any>>;
50
+ receiveMessages(taskId: string, limit?: number): Promise<Record<string, any>>;
51
+ ackMessages(taskId: string, ackToken?: string | null): Promise<Record<string, any> | undefined>;
52
+ listProjects(): Promise<Record<string, any>>;
53
+ createProject(name: string, description?: string, metadata?: Record<string, unknown>): Promise<Record<string, any>>;
54
+ listTasks(payload?: Record<string, any>): Promise<Record<string, any>>;
55
+ getLocalProjectRecord(payload?: Record<string, any>): Promise<Record<string, any>>;
56
+ matchProjectByPath(payload?: Record<string, any>): Promise<Record<string, any>>;
57
+ bindProjectPath(projectId: string, payload?: Record<string, any>): Promise<Record<string, any>>;
58
+ private readonly handleBackendEvent;
59
+ private sendEnvelope;
60
+ private resolveHostname;
61
+ private waitForTaskCreation;
62
+ private readIntEnv;
63
+ }
64
+ export {};
@@ -1,52 +1,77 @@
1
1
  import crypto from 'node:crypto';
2
- import { SessionDiskStore, currentHostname, currentSessionId } from '../session/store.js';
2
+ import { BackendApiClient } from './backend/index.js';
3
+ import { loadConfig } from './config/index.js';
4
+ import { MessageRouter } from './message/index.js';
5
+ import { SessionDiskStore, SessionManager, currentHostname, currentSessionId } from './session/index.js';
6
+ import { ConductorWebSocketClient } from './ws/index.js';
3
7
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4
- export class MCPServer {
8
+ export class ConductorClient {
5
9
  config;
6
- options;
7
- tools;
8
- sessionStore;
9
10
  env;
10
- constructor(config, options) {
11
- this.config = config;
12
- this.options = options;
13
- // Use backend URL to determine session file path (isolates different environments)
14
- this.sessionStore = options.sessionStore ?? SessionDiskStore.forBackendUrl(config.backendUrl);
15
- this.env = options.env ?? process.env;
16
- this.tools = {
17
- create_task_session: this.toolCreateTaskSession,
18
- send_message: this.toolSendMessage,
19
- send_task_status: this.toolSendTaskStatus,
20
- send_runtime_status: this.toolSendRuntimeStatus,
21
- receive_messages: this.toolReceiveMessages,
22
- ack_messages: this.toolAckMessages,
23
- list_projects: this.toolListProjects,
24
- create_project: this.toolCreateProject,
25
- list_tasks: this.toolListTasks,
26
- get_local_project_id: this.toolGetLocalProjectId,
27
- match_project_by_path: this.toolMatchProjectByPath,
28
- bind_project_path: this.toolBindProjectPath,
29
- };
11
+ projectPath;
12
+ backendApi;
13
+ wsClient;
14
+ sessions;
15
+ sessionStore;
16
+ messageRouter;
17
+ agentHost;
18
+ closed = false;
19
+ constructor(init) {
20
+ this.config = init.config;
21
+ this.env = init.env;
22
+ this.projectPath = init.projectPath;
23
+ this.backendApi = init.backendApi;
24
+ this.wsClient = init.wsClient;
25
+ this.sessions = init.sessionManager;
26
+ this.sessionStore = init.sessionStore;
27
+ this.messageRouter = init.messageRouter;
28
+ this.agentHost = init.agentHost;
29
+ this.wsClient.registerHandler(this.handleBackendEvent);
30
+ }
31
+ static async connect(options = {}) {
32
+ const env = options.extraEnv ?? options.env ?? process.env;
33
+ const config = options.config ?? loadConfig(options.configFile, { env });
34
+ const projectPath = options.projectPath ?? process.cwd();
35
+ const backendApi = options.backendApi ?? new BackendApiClient(config);
36
+ const sessions = options.sessionManager ?? new SessionManager();
37
+ const sessionStore = options.sessionStore ?? SessionDiskStore.forBackendUrl(config.backendUrl);
38
+ const messageRouter = options.messageRouter ?? new MessageRouter(sessions);
39
+ const agentHost = resolveAgentHost(env, options.agentHost);
40
+ const wsClient = options.wsClient ??
41
+ new ConductorWebSocketClient(config, {
42
+ hostName: agentHost,
43
+ });
44
+ const client = new ConductorClient({
45
+ config,
46
+ env,
47
+ projectPath,
48
+ backendApi,
49
+ wsClient,
50
+ sessionManager: sessions,
51
+ sessionStore,
52
+ messageRouter,
53
+ agentHost,
54
+ });
55
+ await client.wsClient.connect();
56
+ return client;
30
57
  }
31
- async handleRequest(toolName, payload) {
32
- const handler = this.tools[toolName];
33
- if (!handler) {
34
- throw new Error(`Unknown tool: ${toolName}`);
58
+ async close() {
59
+ if (this.closed) {
60
+ return;
35
61
  }
36
- return handler.call(this, payload);
62
+ this.closed = true;
63
+ await this.wsClient.disconnect();
37
64
  }
38
- async toolCreateTaskSession(payload) {
39
- const projectId = String(payload.project_id || '');
65
+ async createTaskSession(payload) {
66
+ const projectId = String(payload.project_id || '').trim();
40
67
  if (!projectId) {
41
68
  throw new Error('project_id is required');
42
69
  }
43
70
  const title = String(payload.task_title || 'Untitled');
44
- const taskId = String(payload.task_id || crypto.randomUUID());
71
+ const taskId = String(payload.task_id || safeRandomUuid());
45
72
  const sessionId = String(payload.session_id || taskId);
46
- console.error(`[mcp] create_task_session task=${taskId} project=${projectId} title=${title} session=${sessionId}`);
47
- await this.options.sessionManager.addSession(taskId, sessionId, projectId);
48
- // Create task in database via HTTP API
49
- await this.options.backendApi.createTask({
73
+ await this.sessions.addSession(taskId, sessionId, projectId);
74
+ await this.backendApi.createTask({
50
75
  id: taskId,
51
76
  projectId,
52
77
  title,
@@ -55,17 +80,17 @@ export class MCPServer {
55
80
  : typeof payload.backendType === 'string'
56
81
  ? payload.backendType
57
82
  : undefined,
58
- initialContent: payload.prefill,
83
+ initialContent: typeof payload.prefill === 'string' ? payload.prefill : undefined,
59
84
  agentHost: typeof payload.agent_host === 'string'
60
85
  ? payload.agent_host
61
86
  : typeof payload.agentHost === 'string'
62
87
  ? payload.agentHost
63
- : this.options.agentHost,
88
+ : this.agentHost,
64
89
  });
65
90
  await this.waitForTaskCreation(projectId, taskId);
66
91
  const projectPath = typeof payload.project_path === 'string' && payload.project_path
67
92
  ? payload.project_path
68
- : process.cwd();
93
+ : this.projectPath;
69
94
  this.sessionStore.upsert({
70
95
  projectId,
71
96
  taskId,
@@ -79,80 +104,61 @@ export class MCPServer {
79
104
  app_url: payload.app_url,
80
105
  };
81
106
  }
82
- async toolSendMessage(payload) {
83
- const taskId = String(payload.task_id || '');
84
- if (!taskId) {
85
- throw new Error('task_id required');
86
- }
87
- await this.options.backendSender({
107
+ async sendMessage(taskId, content, metadata) {
108
+ await this.sendEnvelope({
88
109
  type: 'sdk_message',
89
110
  payload: {
90
111
  task_id: taskId,
91
- content: payload.content,
92
- metadata: payload.metadata,
112
+ content,
113
+ metadata,
93
114
  },
94
115
  });
95
116
  return { delivered: true };
96
117
  }
97
- async toolSendTaskStatus(payload) {
98
- const taskId = String(payload.task_id || '');
99
- if (!taskId) {
100
- throw new Error('task_id required');
101
- }
102
- await this.options.backendSender({
118
+ async sendTaskStatus(taskId, payload) {
119
+ await this.sendEnvelope({
103
120
  type: 'task_status_update',
104
121
  payload: {
105
122
  task_id: taskId,
106
- status: payload.status,
107
- summary: payload.summary,
123
+ status: payload?.status,
124
+ summary: payload?.summary,
108
125
  },
109
126
  });
110
127
  return { delivered: true };
111
128
  }
112
- async toolSendRuntimeStatus(payload) {
113
- const taskId = String(payload.task_id || '');
114
- if (!taskId) {
115
- throw new Error('task_id required');
116
- }
117
- await this.options.backendSender({
129
+ async sendRuntimeStatus(taskId, payload) {
130
+ await this.sendEnvelope({
118
131
  type: 'task_runtime_status',
119
132
  payload: {
120
133
  task_id: taskId,
121
- state: payload.state,
122
- phase: payload.phase,
123
- source: payload.source,
124
- reply_in_progress: payload.reply_in_progress,
125
- status_line: payload.status_line,
126
- status_done_line: payload.status_done_line,
127
- reply_preview: payload.reply_preview,
128
- reply_to: payload.reply_to,
129
- backend: payload.backend,
130
- thread_id: payload.thread_id,
131
- created_at: payload.created_at,
134
+ state: payload?.state,
135
+ phase: payload?.phase,
136
+ source: payload?.source,
137
+ reply_in_progress: payload?.reply_in_progress,
138
+ status_line: payload?.status_line,
139
+ status_done_line: payload?.status_done_line,
140
+ reply_preview: payload?.reply_preview,
141
+ reply_to: payload?.reply_to,
142
+ backend: payload?.backend,
143
+ thread_id: payload?.thread_id,
144
+ created_at: payload?.created_at,
132
145
  },
133
146
  });
134
147
  return { delivered: true };
135
148
  }
136
- async toolReceiveMessages(payload) {
137
- const taskId = String(payload.task_id || '');
138
- if (!taskId) {
139
- throw new Error('task_id required');
140
- }
141
- const limit = typeof payload.limit === 'number' ? payload.limit : 20;
142
- const messages = await this.options.sessionManager.popMessages(taskId, limit);
149
+ async receiveMessages(taskId, limit = 20) {
150
+ const messages = await this.sessions.popMessages(taskId, limit);
143
151
  return formatMessagesResponse(messages);
144
152
  }
145
- async toolAckMessages(payload) {
146
- const taskId = String(payload.task_id || '');
147
- const ackToken = String(payload.ack_token || '');
148
- if (!taskId || !ackToken) {
149
- throw new Error('task_id and ack_token required');
153
+ async ackMessages(taskId, ackToken) {
154
+ if (!ackToken) {
155
+ return undefined;
150
156
  }
151
- const success = await this.options.sessionManager.ack(taskId, ackToken);
157
+ const success = await this.sessions.ack(taskId, ackToken);
152
158
  return { status: success ? 'ok' : 'ignored' };
153
159
  }
154
- async toolListProjects(_payload) {
155
- const projects = await this.options.backendApi.listProjects();
160
+ async listProjects() {
161
+ const projects = await this.backendApi.listProjects();
156
162
  return {
157
163
  projects: projects.map((project) => typeof project.asObject === 'function'
158
164
  ? project.asObject()
@@ -163,22 +169,12 @@ export class MCPServer {
163
169
  }),
164
170
  };
165
171
  }
166
- async toolCreateProject(payload) {
167
- const name = String(payload.name || '').trim();
168
- if (!name) {
169
- throw new Error('name is required');
170
- }
171
- const description = payload.description ? String(payload.description) : undefined;
172
- const metadata = payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : undefined;
173
- const project = await this.options.backendApi.createProject({
174
- name,
175
- description,
176
- metadata,
177
- });
172
+ async createProject(name, description, metadata) {
173
+ const project = await this.backendApi.createProject({ name, description, metadata });
178
174
  return typeof project.asObject === 'function' ? project.asObject() : project;
179
175
  }
180
- async toolListTasks(payload) {
181
- const tasks = await this.options.backendApi.listTasks({
176
+ async listTasks(payload = {}) {
177
+ const tasks = await this.backendApi.listTasks({
182
178
  projectId: payload.project_id ? String(payload.project_id) : undefined,
183
179
  status: payload.status ? String(payload.status) : undefined,
184
180
  });
@@ -193,10 +189,10 @@ export class MCPServer {
193
189
  })),
194
190
  };
195
191
  }
196
- async toolGetLocalProjectId(payload) {
192
+ async getLocalProjectRecord(payload = {}) {
197
193
  const projectPath = typeof payload.project_path === 'string' && payload.project_path
198
194
  ? payload.project_path
199
- : process.cwd();
195
+ : this.projectPath;
200
196
  const record = this.sessionStore.findByPath(projectPath);
201
197
  if (!record) {
202
198
  throw new Error(`No session record found for project path ${projectPath}`);
@@ -208,13 +204,12 @@ export class MCPServer {
208
204
  hostname: record.hostname,
209
205
  };
210
206
  }
211
- async toolMatchProjectByPath(payload) {
207
+ async matchProjectByPath(payload = {}) {
212
208
  const hostname = typeof payload.hostname === 'string' ? payload.hostname : currentHostname();
213
209
  const projectPath = typeof payload.project_path === 'string' && payload.project_path
214
210
  ? payload.project_path
215
- : process.cwd();
216
- console.error(`[mcp] match_project_by_path hostname=${hostname} path=${projectPath}`);
217
- const result = await this.options.backendApi.matchProjectByPath({
211
+ : this.projectPath;
212
+ const result = await this.backendApi.matchProjectByPath({
218
213
  hostname,
219
214
  path: projectPath,
220
215
  });
@@ -231,31 +226,32 @@ export class MCPServer {
231
226
  matched_path: null,
232
227
  };
233
228
  }
234
- async toolBindProjectPath(payload) {
235
- const projectId = String(payload.project_id || '');
229
+ async bindProjectPath(projectId, payload = {}) {
236
230
  if (!projectId) {
237
231
  throw new Error('project_id is required');
238
232
  }
239
233
  const hostname = typeof payload.hostname === 'string' ? payload.hostname : currentHostname();
240
234
  const projectPath = typeof payload.project_path === 'string' && payload.project_path
241
235
  ? payload.project_path
242
- : process.cwd();
243
- console.error(`[mcp] bind_project_path project=${projectId} hostname=${hostname} path=${projectPath}`);
244
- // Get current project metadata
245
- const project = await this.options.backendApi.getProject(projectId);
236
+ : this.projectPath;
237
+ const project = await this.backendApi.getProject(projectId);
246
238
  const metadata = (project.metadata || {});
247
239
  const localPaths = (metadata.localPaths || {});
248
- // Update localPaths with new binding
249
240
  localPaths[hostname] = projectPath;
250
241
  metadata.localPaths = localPaths;
251
- // Update project
252
- await this.options.backendApi.updateProject(projectId, { metadata });
242
+ await this.backendApi.updateProject(projectId, { metadata });
253
243
  return {
254
244
  success: true,
255
245
  hostname,
256
246
  path: projectPath,
257
247
  };
258
248
  }
249
+ handleBackendEvent = async (payload) => {
250
+ await this.messageRouter.handleBackendEvent(payload);
251
+ };
252
+ async sendEnvelope(envelope) {
253
+ await this.wsClient.sendJson(envelope);
254
+ }
259
255
  resolveHostname() {
260
256
  const records = this.sessionStore.load();
261
257
  for (const record of records) {
@@ -273,21 +269,21 @@ export class MCPServer {
273
269
  const delayMs = this.readIntEnv('CONDUCTOR_TASK_CREATE_DELAY_MS', 250);
274
270
  for (let attempt = 0; attempt < retries; attempt += 1) {
275
271
  try {
276
- const tasks = await this.options.backendApi.listTasks({ projectId });
272
+ const tasks = await this.backendApi.listTasks({ projectId });
277
273
  if (tasks.some((task) => String(task?.id || '') === taskId)) {
278
274
  return;
279
275
  }
280
276
  }
281
277
  catch (error) {
282
278
  const message = error instanceof Error ? error.message : String(error);
283
- console.warn(`[mcp] create_task_session unable to confirm task ${taskId}: ${message}`);
279
+ console.warn(`[sdk] createTaskSession unable to confirm task ${taskId}: ${message}`);
284
280
  return;
285
281
  }
286
282
  if (attempt < retries - 1) {
287
283
  await sleep(delayMs);
288
284
  }
289
285
  }
290
- console.warn(`[mcp] create_task_session timed out waiting for task ${taskId}`);
286
+ console.warn(`[sdk] createTaskSession timed out waiting for task ${taskId}`);
291
287
  }
292
288
  readIntEnv(key, fallback) {
293
289
  const raw = this.env[key];
@@ -311,3 +307,25 @@ function formatMessagesResponse(messages) {
311
307
  has_more: false,
312
308
  };
313
309
  }
310
+ function safeRandomUuid() {
311
+ if (typeof crypto.randomUUID === 'function') {
312
+ return crypto.randomUUID();
313
+ }
314
+ return crypto.randomBytes(16).toString('hex');
315
+ }
316
+ function resolveAgentHost(env, explicit) {
317
+ if (explicit && explicit.trim()) {
318
+ return explicit.trim();
319
+ }
320
+ const fromAgent = env.CONDUCTOR_AGENT_NAME;
321
+ if (typeof fromAgent === 'string' && fromAgent.trim()) {
322
+ return fromAgent.trim();
323
+ }
324
+ const fromDaemon = env.CONDUCTOR_DAEMON_NAME;
325
+ if (typeof fromDaemon === 'string' && fromDaemon.trim()) {
326
+ return fromDaemon.trim();
327
+ }
328
+ const pid = process.pid;
329
+ const host = env.HOSTNAME || env.COMPUTERNAME || 'unknown-host';
330
+ return `conductor-fire-${host}-${pid}`;
331
+ }
package/dist/index.d.ts CHANGED
@@ -3,7 +3,5 @@ export * from './backend/index.js';
3
3
  export * from './ws/index.js';
4
4
  export * from './session/index.js';
5
5
  export * from './message/index.js';
6
+ export * from './client.js';
6
7
  export * from './context/index.js';
7
- export * from './mcp/index.js';
8
- export * from './reporter/index.js';
9
- export * from './orchestrator.js';
package/dist/index.js CHANGED
@@ -3,7 +3,5 @@ export * from './backend/index.js';
3
3
  export * from './ws/index.js';
4
4
  export * from './session/index.js';
5
5
  export * from './message/index.js';
6
+ export * from './client.js';
6
7
  export * from './context/index.js';
7
- export * from './mcp/index.js';
8
- export * from './reporter/index.js';
9
- export * from './orchestrator.js';
@@ -1,12 +1,14 @@
1
1
  import { SessionManager } from '../session/manager.js';
2
- import { MCPNotifier } from '../mcp/notifications.js';
3
2
  export type BackendPayload = Record<string, any>;
4
3
  export type OutboundHandler = (payload: BackendPayload) => Promise<void> | void;
4
+ export interface MessageNotifier {
5
+ notifyNewMessage(taskId: string): Promise<void>;
6
+ }
5
7
  export declare class MessageRouter {
6
8
  private readonly sessions;
7
9
  private readonly notifier?;
8
10
  private readonly outboundHandlers;
9
- constructor(sessions: SessionManager, notifier?: MCPNotifier | undefined);
11
+ constructor(sessions: SessionManager, notifier?: MessageNotifier | undefined);
10
12
  registerOutboundHandler(handler: OutboundHandler): void;
11
13
  handleBackendEvent(payload: BackendPayload): Promise<void>;
12
14
  sendToBackend(payload: BackendPayload): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-sdk",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -17,7 +17,6 @@
17
17
  "prepublishOnly": "npm run build"
18
18
  },
19
19
  "dependencies": {
20
- "@modelcontextprotocol/sdk": "^1.23.0",
21
20
  "ws": "^8.18.0",
22
21
  "yaml": "^2.6.0",
23
22
  "zod": "^3.24.1"
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
@@ -1,222 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
- import { loadConfig, SessionManager, MessageRouter, BackendApiClient, ConductorWebSocketClient, MCPServer, EventReporter, SDKOrchestrator, MCPNotifier, } from '../index.js';
6
- async function main() {
7
- const config = loadConfig();
8
- const sessions = new SessionManager();
9
- const notifier = new MCPNotifier();
10
- const router = new MessageRouter(sessions, notifier);
11
- const backendApi = new BackendApiClient(config);
12
- const configuredAgentHost = typeof process.env.CONDUCTOR_AGENT_NAME === 'string' && process.env.CONDUCTOR_AGENT_NAME.trim()
13
- ? process.env.CONDUCTOR_AGENT_NAME.trim()
14
- : typeof process.env.CONDUCTOR_DAEMON_NAME === 'string' && process.env.CONDUCTOR_DAEMON_NAME.trim()
15
- ? process.env.CONDUCTOR_DAEMON_NAME.trim()
16
- : defaultConductorFireHostName();
17
- const wsClient = new ConductorWebSocketClient(config, { hostName: configuredAgentHost });
18
- const backendSender = async (envelope) => {
19
- await wsClient.sendJson(envelope);
20
- };
21
- const reporter = new EventReporter(backendSender);
22
- const mcpServerLogic = new MCPServer(config, {
23
- sessionManager: sessions,
24
- messageRouter: router,
25
- backendSender,
26
- backendApi,
27
- agentHost: configuredAgentHost,
28
- });
29
- const orchestrator = new SDKOrchestrator({
30
- wsClient,
31
- messageRouter: router,
32
- sessionManager: sessions,
33
- mcpServer: mcpServerLogic,
34
- reporter,
35
- });
36
- const server = new Server({
37
- name: 'conductor-ts',
38
- version: '0.1.0',
39
- }, {
40
- capabilities: {
41
- tools: {},
42
- },
43
- });
44
- server.setRequestHandler(ListToolsRequestSchema, async () => {
45
- return {
46
- tools: [
47
- {
48
- name: 'create_task_session',
49
- description: 'Create a new task session',
50
- inputSchema: {
51
- type: 'object',
52
- properties: {
53
- project_id: { type: 'string' },
54
- task_title: { type: 'string' },
55
- agent_host: { type: 'string' },
56
- prefill: { type: 'string' },
57
- project_path: { type: 'string' },
58
- },
59
- required: ['project_id'],
60
- },
61
- },
62
- {
63
- name: 'list_projects',
64
- description: 'List available projects',
65
- inputSchema: {
66
- type: 'object',
67
- properties: {},
68
- },
69
- },
70
- {
71
- name: 'create_project',
72
- description: 'Create a project',
73
- inputSchema: {
74
- type: 'object',
75
- properties: {
76
- name: { type: 'string' },
77
- description: { type: 'string' },
78
- metadata: { type: 'object' },
79
- },
80
- required: ['name'],
81
- },
82
- },
83
- {
84
- name: 'list_tasks',
85
- description: 'List tasks',
86
- inputSchema: {
87
- type: 'object',
88
- properties: {
89
- project_id: { type: 'string' },
90
- status: { type: 'string' },
91
- },
92
- },
93
- },
94
- {
95
- name: 'send_message',
96
- description: 'Send a message to a task',
97
- inputSchema: {
98
- type: 'object',
99
- properties: {
100
- task_id: { type: 'string' },
101
- content: { type: 'string' },
102
- metadata: { type: 'object' },
103
- },
104
- required: ['task_id', 'content'],
105
- },
106
- },
107
- {
108
- name: 'send_task_status',
109
- description: 'Send a terminal or lifecycle task status update',
110
- inputSchema: {
111
- type: 'object',
112
- properties: {
113
- task_id: { type: 'string' },
114
- status: { type: 'string' },
115
- summary: { type: 'string' },
116
- },
117
- required: ['task_id', 'status'],
118
- },
119
- },
120
- {
121
- name: 'send_runtime_status',
122
- description: 'Send a runtime status update to a task',
123
- inputSchema: {
124
- type: 'object',
125
- properties: {
126
- task_id: { type: 'string' },
127
- state: { type: 'string' },
128
- phase: { type: 'string' },
129
- source: { type: 'string' },
130
- reply_in_progress: { type: 'boolean' },
131
- status_line: { type: 'string' },
132
- status_done_line: { type: 'string' },
133
- reply_preview: { type: 'string' },
134
- reply_to: { type: 'string' },
135
- backend: { type: 'string' },
136
- thread_id: { type: 'string' },
137
- created_at: { type: 'string' },
138
- },
139
- required: ['task_id'],
140
- },
141
- },
142
- {
143
- name: 'receive_messages',
144
- description: 'Receive messages from a task',
145
- inputSchema: {
146
- type: 'object',
147
- properties: {
148
- task_id: { type: 'string' },
149
- limit: { type: 'number' },
150
- },
151
- required: ['task_id'],
152
- },
153
- },
154
- {
155
- name: 'ack_messages',
156
- description: 'Acknowledge messages',
157
- inputSchema: {
158
- type: 'object',
159
- properties: {
160
- task_id: { type: 'string' },
161
- ack_token: { type: 'string' },
162
- },
163
- required: ['task_id', 'ack_token'],
164
- },
165
- },
166
- {
167
- name: 'get_local_project_id',
168
- description: 'Get project ID for local path',
169
- inputSchema: {
170
- type: 'object',
171
- properties: {
172
- project_path: { type: 'string' },
173
- },
174
- },
175
- },
176
- ],
177
- };
178
- });
179
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
180
- const { name, arguments: args } = request.params;
181
- try {
182
- const result = await mcpServerLogic.handleRequest(name, args || {});
183
- return {
184
- content: [
185
- {
186
- type: 'text',
187
- text: JSON.stringify(result),
188
- },
189
- ],
190
- };
191
- }
192
- catch (error) {
193
- return {
194
- content: [
195
- {
196
- type: 'text',
197
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
198
- },
199
- ],
200
- isError: true,
201
- };
202
- }
203
- });
204
- await orchestrator.start();
205
- const transport = new StdioServerTransport();
206
- await server.connect(transport);
207
- // Keep alive
208
- process.on('SIGINT', async () => {
209
- await orchestrator.stop();
210
- await server.close();
211
- process.exit(0);
212
- });
213
- }
214
- function defaultConductorFireHostName() {
215
- const pid = process.pid;
216
- const host = process.env.HOSTNAME || process.env.COMPUTERNAME || 'unknown-host';
217
- return `conductor-fire-${host}-${pid}`;
218
- }
219
- main().catch((err) => {
220
- console.error(err);
221
- process.exit(1);
222
- });
@@ -1,2 +0,0 @@
1
- export * from './notifications.js';
2
- export * from './server.js';
package/dist/mcp/index.js DELETED
@@ -1,2 +0,0 @@
1
- export * from './notifications.js';
2
- export * from './server.js';
@@ -1,20 +0,0 @@
1
- export interface LogSession {
2
- sendLogMessage(args: {
3
- level: string;
4
- data: string;
5
- logger?: string;
6
- }): Promise<void>;
7
- }
8
- export interface MCPContext {
9
- requestContext?: {
10
- session?: LogSession;
11
- };
12
- }
13
- export declare class MCPNotifier {
14
- private session?;
15
- private readonly lock;
16
- bindContext(ctx?: MCPContext | null): Promise<void>;
17
- setSession(session: LogSession): Promise<void>;
18
- notifyNewMessage(taskId: string): Promise<void>;
19
- private getSession;
20
- }
@@ -1,44 +0,0 @@
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 MCPNotifier {
12
- session;
13
- lock = new AsyncLock();
14
- async bindContext(ctx) {
15
- if (!ctx?.requestContext?.session) {
16
- return;
17
- }
18
- await this.setSession(ctx.requestContext.session);
19
- }
20
- async setSession(session) {
21
- await this.lock.runExclusive(async () => {
22
- this.session = session;
23
- });
24
- }
25
- async notifyNewMessage(taskId) {
26
- const session = await this.getSession();
27
- if (!session) {
28
- return;
29
- }
30
- try {
31
- await session.sendLogMessage({
32
- level: 'info',
33
- data: `任务 ${taskId} 收到了新的消息,请调用 receive_messages 工具查看。`,
34
- logger: 'conductor.notifications',
35
- });
36
- }
37
- catch {
38
- // Best-effort notification; ignore errors.
39
- }
40
- }
41
- async getSession() {
42
- return this.lock.runExclusive(async () => this.session);
43
- }
44
- }
@@ -1,42 +0,0 @@
1
- import { BackendPayload, MessageRouter } from '../message/router.js';
2
- import { SessionManager } from '../session/manager.js';
3
- import { SessionDiskStore } from '../session/store.js';
4
- import { ConductorConfig } from '../config/index.js';
5
- import { BackendApiClient } from '../backend/index.js';
6
- type BackendSender = (payload: BackendPayload) => Promise<void>;
7
- export interface MCPServerOptions {
8
- sessionManager: SessionManager;
9
- messageRouter: MessageRouter;
10
- backendSender: BackendSender;
11
- backendApi: Pick<BackendApiClient, 'listProjects' | 'listTasks' | 'createProject' | 'createTask' | 'matchProjectByPath' | 'getProject' | 'updateProject'>;
12
- sessionStore?: SessionDiskStore;
13
- env?: Record<string, string | undefined>;
14
- agentHost?: string;
15
- }
16
- type ToolRequest = Record<string, any>;
17
- type ToolResponse = Record<string, any>;
18
- export declare class MCPServer {
19
- private readonly config;
20
- private readonly options;
21
- private readonly tools;
22
- private readonly sessionStore;
23
- private readonly env;
24
- constructor(config: ConductorConfig, options: MCPServerOptions);
25
- handleRequest(toolName: string, payload: ToolRequest): Promise<ToolResponse>;
26
- private toolCreateTaskSession;
27
- private toolSendMessage;
28
- private toolSendTaskStatus;
29
- private toolSendRuntimeStatus;
30
- private toolReceiveMessages;
31
- private toolAckMessages;
32
- private toolListProjects;
33
- private toolCreateProject;
34
- private toolListTasks;
35
- private toolGetLocalProjectId;
36
- private toolMatchProjectByPath;
37
- private toolBindProjectPath;
38
- private resolveHostname;
39
- private waitForTaskCreation;
40
- private readIntEnv;
41
- }
42
- export {};
@@ -1,21 +0,0 @@
1
- import { MessageRouter } from './message/index.js';
2
- import { EventReporter } from './reporter/index.js';
3
- import { SessionManager } from './session/index.js';
4
- import { ConductorWebSocketClient } from './ws/client.js';
5
- import { MCPServer } from './mcp/server.js';
6
- export interface OrchestratorDeps {
7
- wsClient: ConductorWebSocketClient;
8
- messageRouter: MessageRouter;
9
- sessionManager: SessionManager;
10
- mcpServer: MCPServer;
11
- reporter: EventReporter;
12
- }
13
- export declare class SDKOrchestrator {
14
- private readonly deps;
15
- private readonly wsClient;
16
- private readonly router;
17
- constructor(deps: OrchestratorDeps);
18
- start(): Promise<void>;
19
- stop(): Promise<void>;
20
- private handleBackendEvent;
21
- }
@@ -1,20 +0,0 @@
1
- export class SDKOrchestrator {
2
- deps;
3
- wsClient;
4
- router;
5
- constructor(deps) {
6
- this.deps = deps;
7
- this.wsClient = deps.wsClient;
8
- this.router = deps.messageRouter;
9
- this.wsClient.registerHandler((payload) => this.handleBackendEvent(payload));
10
- }
11
- async start() {
12
- await this.wsClient.connect();
13
- }
14
- async stop() {
15
- await this.wsClient.disconnect();
16
- }
17
- async handleBackendEvent(payload) {
18
- await this.router.handleBackendEvent(payload);
19
- }
20
- }
@@ -1,7 +0,0 @@
1
- export type BackendSender = (payload: Record<string, any>) => Promise<void>;
2
- export declare class EventReporter {
3
- private readonly backendSender;
4
- constructor(backendSender: BackendSender);
5
- emit(eventType: string, payload: Record<string, any>): Promise<void>;
6
- taskStatus(taskId: string, status: string, summary?: string | null): Promise<void>;
7
- }
@@ -1,20 +0,0 @@
1
- export class EventReporter {
2
- backendSender;
3
- constructor(backendSender) {
4
- this.backendSender = backendSender;
5
- }
6
- async emit(eventType, payload) {
7
- await this.backendSender({
8
- type: eventType,
9
- timestamp: new Date().toISOString(),
10
- payload,
11
- });
12
- }
13
- async taskStatus(taskId, status, summary) {
14
- await this.emit('task_status_update', {
15
- task_id: taskId,
16
- status,
17
- summary: summary ?? undefined,
18
- });
19
- }
20
- }
@@ -1 +0,0 @@
1
- export * from './event_stream.js';
@@ -1 +0,0 @@
1
- export * from './event_stream.js';