@love-moon/conductor-sdk 0.2.6 → 0.2.8

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,80 @@
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
+ onConnected?: (event: {
21
+ isReconnect: boolean;
22
+ }) => void;
23
+ onDisconnected?: () => void;
24
+ }
25
+ interface ConductorClientInit {
26
+ config: ConductorConfig;
27
+ env: Record<string, string | undefined>;
28
+ projectPath: string;
29
+ backendApi: BackendApiLike;
30
+ wsClient: RealtimeClientLike;
31
+ sessionManager: SessionManager;
32
+ sessionStore: SessionDiskStore;
33
+ messageRouter: MessageRouter;
34
+ agentHost: string;
35
+ }
36
+ export declare class ConductorClient {
37
+ private readonly config;
38
+ private readonly env;
39
+ private readonly projectPath;
40
+ private readonly backendApi;
41
+ private readonly wsClient;
42
+ private readonly sessions;
43
+ private readonly sessionStore;
44
+ private readonly messageRouter;
45
+ private readonly agentHost;
46
+ private closed;
47
+ constructor(init: ConductorClientInit);
48
+ static connect(options?: ConductorClientConnectOptions): Promise<ConductorClient>;
49
+ close(): Promise<void>;
50
+ createTaskSession(payload: Record<string, any>): Promise<Record<string, any>>;
51
+ sendMessage(taskId: string, content: string, metadata?: Record<string, any>): Promise<Record<string, any>>;
52
+ sendTaskStatus(taskId: string, payload: Record<string, any>): Promise<Record<string, any>>;
53
+ sendRuntimeStatus(taskId: string, payload: Record<string, any>): Promise<Record<string, any>>;
54
+ sendAgentResume(payload?: {
55
+ active_tasks?: string[];
56
+ source?: string;
57
+ metadata?: Record<string, any>;
58
+ }): Promise<Record<string, any>>;
59
+ sendAgentCommandAck(payload: {
60
+ request_id: string;
61
+ task_id?: string;
62
+ event_type?: string;
63
+ accepted?: boolean;
64
+ }): Promise<Record<string, any>>;
65
+ receiveMessages(taskId: string, limit?: number): Promise<Record<string, any>>;
66
+ ackMessages(taskId: string, ackToken?: string | null): Promise<Record<string, any> | undefined>;
67
+ listProjects(): Promise<Record<string, any>>;
68
+ createProject(name: string, description?: string, metadata?: Record<string, unknown>): Promise<Record<string, any>>;
69
+ listTasks(payload?: Record<string, any>): Promise<Record<string, any>>;
70
+ getLocalProjectRecord(payload?: Record<string, any>): Promise<Record<string, any>>;
71
+ matchProjectByPath(payload?: Record<string, any>): Promise<Record<string, any>>;
72
+ bindProjectPath(projectId: string, payload?: Record<string, any>): Promise<Record<string, any>>;
73
+ private readonly handleBackendEvent;
74
+ private sendEnvelope;
75
+ private maybeAckInboundCommand;
76
+ private resolveHostname;
77
+ private waitForTaskCreation;
78
+ private readIntEnv;
79
+ }
80
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,391 @@
1
+ import crypto from 'node:crypto';
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';
7
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8
+ export class ConductorClient {
9
+ config;
10
+ env;
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
+ onConnected: options.onConnected,
44
+ onDisconnected: options.onDisconnected,
45
+ });
46
+ const client = new ConductorClient({
47
+ config,
48
+ env,
49
+ projectPath,
50
+ backendApi,
51
+ wsClient,
52
+ sessionManager: sessions,
53
+ sessionStore,
54
+ messageRouter,
55
+ agentHost,
56
+ });
57
+ await client.wsClient.connect();
58
+ return client;
59
+ }
60
+ async close() {
61
+ if (this.closed) {
62
+ return;
63
+ }
64
+ this.closed = true;
65
+ await this.wsClient.disconnect();
66
+ }
67
+ async createTaskSession(payload) {
68
+ const projectId = String(payload.project_id || '').trim();
69
+ if (!projectId) {
70
+ throw new Error('project_id is required');
71
+ }
72
+ const title = String(payload.task_title || 'Untitled');
73
+ const taskId = String(payload.task_id || safeRandomUuid());
74
+ const sessionId = String(payload.session_id || taskId);
75
+ await this.sessions.addSession(taskId, sessionId, projectId);
76
+ await this.backendApi.createTask({
77
+ id: taskId,
78
+ projectId,
79
+ title,
80
+ backendType: typeof payload.backend_type === 'string'
81
+ ? payload.backend_type
82
+ : typeof payload.backendType === 'string'
83
+ ? payload.backendType
84
+ : undefined,
85
+ initialContent: typeof payload.prefill === 'string' ? payload.prefill : undefined,
86
+ agentHost: typeof payload.agent_host === 'string'
87
+ ? payload.agent_host
88
+ : typeof payload.agentHost === 'string'
89
+ ? payload.agentHost
90
+ : this.agentHost,
91
+ });
92
+ await this.waitForTaskCreation(projectId, taskId);
93
+ const projectPath = typeof payload.project_path === 'string' && payload.project_path
94
+ ? payload.project_path
95
+ : this.projectPath;
96
+ this.sessionStore.upsert({
97
+ projectId,
98
+ taskId,
99
+ projectPath,
100
+ sessionId: currentSessionId(this.env),
101
+ hostname: this.resolveHostname(),
102
+ });
103
+ return {
104
+ task_id: taskId,
105
+ session_id: sessionId,
106
+ app_url: payload.app_url,
107
+ };
108
+ }
109
+ async sendMessage(taskId, content, metadata) {
110
+ await this.sendEnvelope({
111
+ type: 'sdk_message',
112
+ payload: {
113
+ task_id: taskId,
114
+ content,
115
+ metadata,
116
+ },
117
+ });
118
+ return { delivered: true };
119
+ }
120
+ async sendTaskStatus(taskId, payload) {
121
+ await this.sendEnvelope({
122
+ type: 'task_status_update',
123
+ payload: {
124
+ task_id: taskId,
125
+ status: payload?.status,
126
+ summary: payload?.summary,
127
+ },
128
+ });
129
+ return { delivered: true };
130
+ }
131
+ async sendRuntimeStatus(taskId, payload) {
132
+ await this.sendEnvelope({
133
+ type: 'task_runtime_status',
134
+ payload: {
135
+ task_id: taskId,
136
+ state: payload?.state,
137
+ phase: payload?.phase,
138
+ source: payload?.source,
139
+ reply_in_progress: payload?.reply_in_progress,
140
+ status_line: payload?.status_line,
141
+ status_done_line: payload?.status_done_line,
142
+ reply_preview: payload?.reply_preview,
143
+ reply_to: payload?.reply_to,
144
+ backend: payload?.backend,
145
+ thread_id: payload?.thread_id,
146
+ created_at: payload?.created_at,
147
+ },
148
+ });
149
+ return { delivered: true };
150
+ }
151
+ async sendAgentResume(payload = {}) {
152
+ await this.sendEnvelope({
153
+ type: 'agent_resume',
154
+ payload: {
155
+ active_tasks: Array.isArray(payload.active_tasks)
156
+ ? payload.active_tasks.map((taskId) => String(taskId)).filter(Boolean)
157
+ : [],
158
+ source: payload.source,
159
+ metadata: payload.metadata,
160
+ },
161
+ });
162
+ return { delivered: true };
163
+ }
164
+ async sendAgentCommandAck(payload) {
165
+ const requestId = String(payload.request_id || '').trim();
166
+ if (!requestId) {
167
+ throw new Error('request_id is required');
168
+ }
169
+ await this.sendEnvelope({
170
+ type: 'agent_command_ack',
171
+ payload: {
172
+ request_id: requestId,
173
+ task_id: payload.task_id,
174
+ event_type: payload.event_type,
175
+ accepted: payload.accepted !== false,
176
+ },
177
+ });
178
+ return { delivered: true };
179
+ }
180
+ async receiveMessages(taskId, limit = 20) {
181
+ const messages = await this.sessions.popMessages(taskId, limit);
182
+ return formatMessagesResponse(messages);
183
+ }
184
+ async ackMessages(taskId, ackToken) {
185
+ if (!ackToken) {
186
+ return undefined;
187
+ }
188
+ const success = await this.sessions.ack(taskId, ackToken);
189
+ return { status: success ? 'ok' : 'ignored' };
190
+ }
191
+ async listProjects() {
192
+ const projects = await this.backendApi.listProjects();
193
+ return {
194
+ projects: projects.map((project) => typeof project.asObject === 'function'
195
+ ? project.asObject()
196
+ : {
197
+ id: project.id,
198
+ name: project.name ?? null,
199
+ description: project.description ?? null,
200
+ }),
201
+ };
202
+ }
203
+ async createProject(name, description, metadata) {
204
+ const project = await this.backendApi.createProject({ name, description, metadata });
205
+ return typeof project.asObject === 'function' ? project.asObject() : project;
206
+ }
207
+ async listTasks(payload = {}) {
208
+ const tasks = await this.backendApi.listTasks({
209
+ projectId: payload.project_id ? String(payload.project_id) : undefined,
210
+ status: payload.status ? String(payload.status) : undefined,
211
+ });
212
+ return {
213
+ tasks: tasks.map((task) => ({
214
+ id: task.id,
215
+ project_id: task.project_id ?? task.projectId ?? null,
216
+ title: task.title,
217
+ status: task.status,
218
+ created_at: task.created_at ?? task.createdAt ?? null,
219
+ updated_at: task.updated_at ?? task.updatedAt ?? null,
220
+ })),
221
+ };
222
+ }
223
+ async getLocalProjectRecord(payload = {}) {
224
+ const projectPath = typeof payload.project_path === 'string' && payload.project_path
225
+ ? payload.project_path
226
+ : this.projectPath;
227
+ const record = this.sessionStore.findByPath(projectPath);
228
+ if (!record) {
229
+ throw new Error(`No session record found for project path ${projectPath}`);
230
+ }
231
+ return {
232
+ project_id: record.projectId,
233
+ task_id: Array.from(record.taskIds),
234
+ session_id: record.sessionId,
235
+ hostname: record.hostname,
236
+ };
237
+ }
238
+ async matchProjectByPath(payload = {}) {
239
+ const hostname = typeof payload.hostname === 'string' ? payload.hostname : currentHostname();
240
+ const projectPath = typeof payload.project_path === 'string' && payload.project_path
241
+ ? payload.project_path
242
+ : this.projectPath;
243
+ const result = await this.backendApi.matchProjectByPath({
244
+ hostname,
245
+ path: projectPath,
246
+ });
247
+ if (result.project) {
248
+ return {
249
+ project_id: result.project.id,
250
+ project_name: result.project.name,
251
+ matched_path: result.matchedPath,
252
+ };
253
+ }
254
+ return {
255
+ project_id: null,
256
+ project_name: null,
257
+ matched_path: null,
258
+ };
259
+ }
260
+ async bindProjectPath(projectId, payload = {}) {
261
+ if (!projectId) {
262
+ throw new Error('project_id is required');
263
+ }
264
+ const hostname = typeof payload.hostname === 'string' ? payload.hostname : currentHostname();
265
+ const projectPath = typeof payload.project_path === 'string' && payload.project_path
266
+ ? payload.project_path
267
+ : this.projectPath;
268
+ const project = await this.backendApi.getProject(projectId);
269
+ const metadata = (project.metadata || {});
270
+ const localPaths = (metadata.localPaths || {});
271
+ localPaths[hostname] = projectPath;
272
+ metadata.localPaths = localPaths;
273
+ await this.backendApi.updateProject(projectId, { metadata });
274
+ return {
275
+ success: true,
276
+ hostname,
277
+ path: projectPath,
278
+ };
279
+ }
280
+ handleBackendEvent = async (payload) => {
281
+ await this.messageRouter.handleBackendEvent(payload);
282
+ await this.maybeAckInboundCommand(payload);
283
+ };
284
+ async sendEnvelope(envelope) {
285
+ await this.wsClient.sendJson(envelope);
286
+ }
287
+ async maybeAckInboundCommand(payload) {
288
+ const eventType = typeof payload?.type === 'string' ? payload.type : '';
289
+ if (eventType !== 'task_user_message' && eventType !== 'task_action') {
290
+ return;
291
+ }
292
+ const data = payload?.payload && typeof payload.payload === 'object'
293
+ ? payload.payload
294
+ : null;
295
+ if (!data) {
296
+ return;
297
+ }
298
+ const requestId = typeof data.request_id === 'string' ? data.request_id.trim() : '';
299
+ if (!requestId) {
300
+ return;
301
+ }
302
+ try {
303
+ await this.sendAgentCommandAck({
304
+ request_id: requestId,
305
+ task_id: typeof data.task_id === 'string' ? data.task_id : undefined,
306
+ event_type: eventType,
307
+ accepted: true,
308
+ });
309
+ }
310
+ catch (error) {
311
+ const message = error instanceof Error ? error.message : String(error);
312
+ console.warn(`[sdk] failed to ack inbound command ${requestId}: ${message}`);
313
+ }
314
+ }
315
+ resolveHostname() {
316
+ const records = this.sessionStore.load();
317
+ for (const record of records) {
318
+ if (record.hostname) {
319
+ return record.hostname;
320
+ }
321
+ }
322
+ return currentHostname();
323
+ }
324
+ async waitForTaskCreation(projectId, taskId) {
325
+ const retries = this.readIntEnv('CONDUCTOR_TASK_CREATE_RETRIES', 10);
326
+ if (retries <= 0) {
327
+ return;
328
+ }
329
+ const delayMs = this.readIntEnv('CONDUCTOR_TASK_CREATE_DELAY_MS', 250);
330
+ for (let attempt = 0; attempt < retries; attempt += 1) {
331
+ try {
332
+ const tasks = await this.backendApi.listTasks({ projectId });
333
+ if (tasks.some((task) => String(task?.id || '') === taskId)) {
334
+ return;
335
+ }
336
+ }
337
+ catch (error) {
338
+ const message = error instanceof Error ? error.message : String(error);
339
+ console.warn(`[sdk] createTaskSession unable to confirm task ${taskId}: ${message}`);
340
+ return;
341
+ }
342
+ if (attempt < retries - 1) {
343
+ await sleep(delayMs);
344
+ }
345
+ }
346
+ console.warn(`[sdk] createTaskSession timed out waiting for task ${taskId}`);
347
+ }
348
+ readIntEnv(key, fallback) {
349
+ const raw = this.env[key];
350
+ if (!raw) {
351
+ return fallback;
352
+ }
353
+ const value = parseInt(raw, 10);
354
+ return Number.isFinite(value) ? value : fallback;
355
+ }
356
+ }
357
+ function formatMessagesResponse(messages) {
358
+ return {
359
+ messages: messages.map((msg) => ({
360
+ message_id: msg.messageId,
361
+ role: msg.role,
362
+ content: msg.content,
363
+ ack_token: msg.ackToken,
364
+ created_at: msg.createdAt.toISOString(),
365
+ })),
366
+ next_ack_token: messages.length ? messages[messages.length - 1].ackToken ?? null : null,
367
+ has_more: false,
368
+ };
369
+ }
370
+ function safeRandomUuid() {
371
+ if (typeof crypto.randomUUID === 'function') {
372
+ return crypto.randomUUID();
373
+ }
374
+ return crypto.randomBytes(16).toString('hex');
375
+ }
376
+ function resolveAgentHost(env, explicit) {
377
+ if (explicit && explicit.trim()) {
378
+ return explicit.trim();
379
+ }
380
+ const fromAgent = env.CONDUCTOR_AGENT_NAME;
381
+ if (typeof fromAgent === 'string' && fromAgent.trim()) {
382
+ return fromAgent.trim();
383
+ }
384
+ const fromDaemon = env.CONDUCTOR_DAEMON_NAME;
385
+ if (typeof fromDaemon === 'string' && fromDaemon.trim()) {
386
+ return fromDaemon.trim();
387
+ }
388
+ const pid = process.pid;
389
+ const host = env.HOSTNAME || env.COMPUTERNAME || 'unknown-host';
390
+ return `conductor-fire-${host}-${pid}`;
391
+ }
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.6",
3
+ "version": "0.2.8",
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,313 +0,0 @@
1
- import crypto from 'node:crypto';
2
- import { SessionDiskStore, currentHostname, currentSessionId } from '../session/store.js';
3
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4
- export class MCPServer {
5
- config;
6
- options;
7
- tools;
8
- sessionStore;
9
- 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
- };
30
- }
31
- async handleRequest(toolName, payload) {
32
- const handler = this.tools[toolName];
33
- if (!handler) {
34
- throw new Error(`Unknown tool: ${toolName}`);
35
- }
36
- return handler.call(this, payload);
37
- }
38
- async toolCreateTaskSession(payload) {
39
- const projectId = String(payload.project_id || '');
40
- if (!projectId) {
41
- throw new Error('project_id is required');
42
- }
43
- const title = String(payload.task_title || 'Untitled');
44
- const taskId = String(payload.task_id || crypto.randomUUID());
45
- 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({
50
- id: taskId,
51
- projectId,
52
- title,
53
- backendType: typeof payload.backend_type === 'string'
54
- ? payload.backend_type
55
- : typeof payload.backendType === 'string'
56
- ? payload.backendType
57
- : undefined,
58
- initialContent: payload.prefill,
59
- agentHost: typeof payload.agent_host === 'string'
60
- ? payload.agent_host
61
- : typeof payload.agentHost === 'string'
62
- ? payload.agentHost
63
- : this.options.agentHost,
64
- });
65
- await this.waitForTaskCreation(projectId, taskId);
66
- const projectPath = typeof payload.project_path === 'string' && payload.project_path
67
- ? payload.project_path
68
- : process.cwd();
69
- this.sessionStore.upsert({
70
- projectId,
71
- taskId,
72
- projectPath,
73
- sessionId: currentSessionId(this.env),
74
- hostname: this.resolveHostname(),
75
- });
76
- return {
77
- task_id: taskId,
78
- session_id: sessionId,
79
- app_url: payload.app_url,
80
- };
81
- }
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({
88
- type: 'sdk_message',
89
- payload: {
90
- task_id: taskId,
91
- content: payload.content,
92
- metadata: payload.metadata,
93
- },
94
- });
95
- return { delivered: true };
96
- }
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({
103
- type: 'task_status_update',
104
- payload: {
105
- task_id: taskId,
106
- status: payload.status,
107
- summary: payload.summary,
108
- },
109
- });
110
- return { delivered: true };
111
- }
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({
118
- type: 'task_runtime_status',
119
- payload: {
120
- 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,
132
- },
133
- });
134
- return { delivered: true };
135
- }
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);
143
- return formatMessagesResponse(messages);
144
- }
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');
150
- }
151
- const success = await this.options.sessionManager.ack(taskId, ackToken);
152
- return { status: success ? 'ok' : 'ignored' };
153
- }
154
- async toolListProjects(_payload) {
155
- const projects = await this.options.backendApi.listProjects();
156
- return {
157
- projects: projects.map((project) => typeof project.asObject === 'function'
158
- ? project.asObject()
159
- : {
160
- id: project.id,
161
- name: project.name ?? null,
162
- description: project.description ?? null,
163
- }),
164
- };
165
- }
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
- });
178
- return typeof project.asObject === 'function' ? project.asObject() : project;
179
- }
180
- async toolListTasks(payload) {
181
- const tasks = await this.options.backendApi.listTasks({
182
- projectId: payload.project_id ? String(payload.project_id) : undefined,
183
- status: payload.status ? String(payload.status) : undefined,
184
- });
185
- return {
186
- tasks: tasks.map((task) => ({
187
- id: task.id,
188
- project_id: task.project_id ?? task.projectId ?? null,
189
- title: task.title,
190
- status: task.status,
191
- created_at: task.created_at ?? task.createdAt ?? null,
192
- updated_at: task.updated_at ?? task.updatedAt ?? null,
193
- })),
194
- };
195
- }
196
- async toolGetLocalProjectId(payload) {
197
- const projectPath = typeof payload.project_path === 'string' && payload.project_path
198
- ? payload.project_path
199
- : process.cwd();
200
- const record = this.sessionStore.findByPath(projectPath);
201
- if (!record) {
202
- throw new Error(`No session record found for project path ${projectPath}`);
203
- }
204
- return {
205
- project_id: record.projectId,
206
- task_id: Array.from(record.taskIds),
207
- session_id: record.sessionId,
208
- hostname: record.hostname,
209
- };
210
- }
211
- async toolMatchProjectByPath(payload) {
212
- const hostname = typeof payload.hostname === 'string' ? payload.hostname : currentHostname();
213
- const projectPath = typeof payload.project_path === 'string' && payload.project_path
214
- ? 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({
218
- hostname,
219
- path: projectPath,
220
- });
221
- if (result.project) {
222
- return {
223
- project_id: result.project.id,
224
- project_name: result.project.name,
225
- matched_path: result.matchedPath,
226
- };
227
- }
228
- return {
229
- project_id: null,
230
- project_name: null,
231
- matched_path: null,
232
- };
233
- }
234
- async toolBindProjectPath(payload) {
235
- const projectId = String(payload.project_id || '');
236
- if (!projectId) {
237
- throw new Error('project_id is required');
238
- }
239
- const hostname = typeof payload.hostname === 'string' ? payload.hostname : currentHostname();
240
- const projectPath = typeof payload.project_path === 'string' && payload.project_path
241
- ? 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);
246
- const metadata = (project.metadata || {});
247
- const localPaths = (metadata.localPaths || {});
248
- // Update localPaths with new binding
249
- localPaths[hostname] = projectPath;
250
- metadata.localPaths = localPaths;
251
- // Update project
252
- await this.options.backendApi.updateProject(projectId, { metadata });
253
- return {
254
- success: true,
255
- hostname,
256
- path: projectPath,
257
- };
258
- }
259
- resolveHostname() {
260
- const records = this.sessionStore.load();
261
- for (const record of records) {
262
- if (record.hostname) {
263
- return record.hostname;
264
- }
265
- }
266
- return currentHostname();
267
- }
268
- async waitForTaskCreation(projectId, taskId) {
269
- const retries = this.readIntEnv('CONDUCTOR_TASK_CREATE_RETRIES', 10);
270
- if (retries <= 0) {
271
- return;
272
- }
273
- const delayMs = this.readIntEnv('CONDUCTOR_TASK_CREATE_DELAY_MS', 250);
274
- for (let attempt = 0; attempt < retries; attempt += 1) {
275
- try {
276
- const tasks = await this.options.backendApi.listTasks({ projectId });
277
- if (tasks.some((task) => String(task?.id || '') === taskId)) {
278
- return;
279
- }
280
- }
281
- catch (error) {
282
- const message = error instanceof Error ? error.message : String(error);
283
- console.warn(`[mcp] create_task_session unable to confirm task ${taskId}: ${message}`);
284
- return;
285
- }
286
- if (attempt < retries - 1) {
287
- await sleep(delayMs);
288
- }
289
- }
290
- console.warn(`[mcp] create_task_session timed out waiting for task ${taskId}`);
291
- }
292
- readIntEnv(key, fallback) {
293
- const raw = this.env[key];
294
- if (!raw) {
295
- return fallback;
296
- }
297
- const value = parseInt(raw, 10);
298
- return Number.isFinite(value) ? value : fallback;
299
- }
300
- }
301
- function formatMessagesResponse(messages) {
302
- return {
303
- messages: messages.map((msg) => ({
304
- message_id: msg.messageId,
305
- role: msg.role,
306
- content: msg.content,
307
- ack_token: msg.ackToken,
308
- created_at: msg.createdAt.toISOString(),
309
- })),
310
- next_ack_token: messages.length ? messages[messages.length - 1].ackToken ?? null : null,
311
- has_more: false,
312
- };
313
- }
@@ -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';