@adamancyzhang/claude-orchestrator 0.2.8 → 0.3.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 (62) hide show
  1. package/dist/cli/commands.d.ts +8 -4
  2. package/dist/cli/commands.js +111 -162
  3. package/dist/cli/commands.js.map +1 -1
  4. package/dist/config.d.ts +6 -0
  5. package/dist/config.js +11 -2
  6. package/dist/config.js.map +1 -1
  7. package/dist/index.js +71 -28
  8. package/dist/index.js.map +1 -1
  9. package/dist/leader/event-bus.d.ts +11 -0
  10. package/dist/leader/event-bus.js +21 -0
  11. package/dist/leader/event-bus.js.map +1 -0
  12. package/dist/leader/index.d.ts +7 -0
  13. package/dist/leader/index.js +86 -0
  14. package/dist/leader/index.js.map +1 -0
  15. package/dist/leader/monitor.d.ts +14 -0
  16. package/dist/leader/monitor.js +55 -0
  17. package/dist/leader/monitor.js.map +1 -0
  18. package/dist/leader/orchestrator.d.ts +14 -0
  19. package/dist/leader/orchestrator.js +83 -0
  20. package/dist/leader/orchestrator.js.map +1 -0
  21. package/dist/leader/recovery.d.ts +9 -0
  22. package/dist/leader/recovery.js +46 -0
  23. package/dist/leader/recovery.js.map +1 -0
  24. package/dist/leader/state.d.ts +23 -0
  25. package/dist/leader/state.js +85 -0
  26. package/dist/leader/state.js.map +1 -0
  27. package/dist/leader/tui.d.ts +5 -0
  28. package/dist/leader/tui.js +133 -0
  29. package/dist/leader/tui.js.map +1 -0
  30. package/dist/leader/watcher.d.ts +16 -0
  31. package/dist/leader/watcher.js +77 -0
  32. package/dist/leader/watcher.js.map +1 -0
  33. package/dist/models/schemas.d.ts +64 -17
  34. package/dist/models/schemas.js +29 -3
  35. package/dist/models/schemas.js.map +1 -1
  36. package/dist/modules/message-router.d.ts +2 -0
  37. package/dist/modules/message-router.js +17 -0
  38. package/dist/modules/message-router.js.map +1 -1
  39. package/dist/modules/task-queue.d.ts +4 -1
  40. package/dist/modules/task-queue.js +87 -9
  41. package/dist/modules/task-queue.js.map +1 -1
  42. package/dist/templates/leader.md +10 -0
  43. package/dist/templates/worker.md +8 -0
  44. package/dist/utils/exec.d.ts +3 -0
  45. package/dist/utils/exec.js +20 -0
  46. package/dist/utils/exec.js.map +1 -0
  47. package/dist/worker/watcher.d.ts +16 -0
  48. package/dist/worker/watcher.js +82 -0
  49. package/dist/worker/watcher.js.map +1 -0
  50. package/dist/zk/client.d.ts +5 -0
  51. package/dist/zk/client.js +18 -1
  52. package/dist/zk/client.js.map +1 -1
  53. package/dist/zk/paths.d.ts +2 -0
  54. package/dist/zk/paths.js +4 -0
  55. package/dist/zk/paths.js.map +1 -1
  56. package/package.json +3 -6
  57. package/dist/modules/message-watcher.d.ts +0 -12
  58. package/dist/modules/message-watcher.js +0 -133
  59. package/dist/modules/message-watcher.js.map +0 -1
  60. package/dist/server.d.ts +0 -2
  61. package/dist/server.js +0 -490
  62. package/dist/server.js.map +0 -1
@@ -1,133 +0,0 @@
1
- import { spawn } from "child_process";
2
- import * as paths from "../zk/paths.js";
3
- import { MessageSchema } from "../models/schemas.js";
4
- export class MessageWatcher {
5
- zk;
6
- instances = new Map();
7
- constructor(zk) {
8
- this.zk = zk;
9
- }
10
- async startWatching(instanceId, workDir) {
11
- this.stopWatching(instanceId);
12
- await this.zk.mkdirp(paths.messageDirPath(instanceId));
13
- const state = {
14
- workDir,
15
- queue: [],
16
- processing: false,
17
- stopped: false,
18
- child: null,
19
- inFlight: new Set(),
20
- };
21
- this.instances.set(instanceId, state);
22
- console.log(`[MessageWatcher] Started watching ${instanceId.slice(0, 8)} at ${workDir}`);
23
- await this._watchLoop(instanceId);
24
- }
25
- stopWatching(instanceId) {
26
- const state = this.instances.get(instanceId);
27
- if (!state)
28
- return;
29
- state.stopped = true;
30
- if (state.child) {
31
- state.child.kill("SIGTERM");
32
- state.child = null;
33
- }
34
- this.instances.delete(instanceId);
35
- console.log(`[MessageWatcher] Stopped watching ${instanceId.slice(0, 8)}`);
36
- }
37
- stopAll() {
38
- for (const id of this.instances.keys()) {
39
- this.stopWatching(id);
40
- }
41
- }
42
- async _watchLoop(instanceId) {
43
- const state = this.instances.get(instanceId);
44
- if (!state || state.stopped)
45
- return;
46
- try {
47
- const children = await this.zk.watchMessageDir(instanceId, async (newChildren) => {
48
- await this._onChildrenChanged(instanceId, newChildren);
49
- this._watchLoop(instanceId);
50
- });
51
- await this._onChildrenChanged(instanceId, children);
52
- }
53
- catch (err) {
54
- console.error(`[MessageWatcher] Watch failed for ${instanceId.slice(0, 8)}:`, err);
55
- this.stopWatching(instanceId);
56
- }
57
- }
58
- async _onChildrenChanged(instanceId, children) {
59
- const state = this.instances.get(instanceId);
60
- if (!state || state.stopped)
61
- return;
62
- for (const msgId of children) {
63
- if (state.inFlight.has(msgId))
64
- continue;
65
- const data = await this.zk.getMessage(instanceId, msgId);
66
- if (!data)
67
- continue;
68
- const msg = MessageSchema.parse({ ...data, id: msgId });
69
- if (!msg.read) {
70
- state.inFlight.add(msgId);
71
- state.queue.push(msg);
72
- }
73
- }
74
- if (!state.processing && state.queue.length > 0) {
75
- this._processQueue(instanceId);
76
- }
77
- }
78
- async _processQueue(instanceId) {
79
- const state = this.instances.get(instanceId);
80
- if (!state || state.stopped)
81
- return;
82
- state.processing = true;
83
- while (state.queue.length > 0 && !state.stopped) {
84
- const msg = state.queue.shift();
85
- const fromLabel = msg.from_name || msg.from_instance?.slice(0, 8) || "unknown";
86
- const prompt = `[${msg.type} from ${fromLabel}] ${msg.content}`;
87
- console.log(`[MessageWatcher] Processing message ${msg.id} for ${instanceId.slice(0, 8)}`);
88
- try {
89
- const child = spawn("claude", ["--session-id", instanceId, "-p", prompt], {
90
- cwd: state.workDir,
91
- stdio: ["ignore", "pipe", "pipe"],
92
- env: { ...process.env },
93
- });
94
- state.child = child;
95
- let stdout = "";
96
- let stderr = "";
97
- child.stdout?.on("data", (d) => (stdout += d.toString()));
98
- child.stderr?.on("data", (d) => (stderr += d.toString()));
99
- const { code, error } = await new Promise((resolve) => {
100
- child.on("exit", (code) => resolve({ code: code ?? -1, error: null }));
101
- child.on("error", (err) => resolve({ code: -1, error: err }));
102
- });
103
- state.child = null;
104
- if (error) {
105
- console.error(`[MessageWatcher] claude failed for ${instanceId.slice(0, 8)}: ${error.message}`);
106
- }
107
- else if (code !== 0) {
108
- console.error(`[MessageWatcher] claude exited ${code} for ${instanceId.slice(0, 8)}: ${stderr.slice(0, 200)}`);
109
- }
110
- else {
111
- console.log(`[MessageWatcher] Message ${msg.id} processed for ${instanceId.slice(0, 8)}`);
112
- }
113
- if (stdout) {
114
- console.log(`[MessageWatcher] Output: ${stdout.slice(0, 500)}`);
115
- }
116
- }
117
- catch (err) {
118
- console.error(`[MessageWatcher] Unexpected error processing ${msg.id}:`, err);
119
- }
120
- // Always mark as read
121
- try {
122
- msg.read = true;
123
- await this.zk.updateMessage(instanceId, msg.id, msg);
124
- }
125
- catch (err) {
126
- console.error(`[MessageWatcher] Failed to mark ${msg.id} as read:`, err);
127
- }
128
- state.inFlight.delete(msg.id);
129
- }
130
- state.processing = false;
131
- }
132
- }
133
- //# sourceMappingURL=message-watcher.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"message-watcher.js","sourceRoot":"","sources":["../../src/modules/message-watcher.ts"],"names":[],"mappings":"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"}
package/dist/server.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import type { Config } from "./config.js";
2
- export declare function startServer(config: Config): Promise<void>;
package/dist/server.js DELETED
@@ -1,490 +0,0 @@
1
- import { z } from "zod";
2
- import express from "express";
3
- import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
4
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
- import { ZkClient } from "./zk/client.js";
6
- import { InstanceRegistry } from "./modules/registry.js";
7
- import { TaskQueue } from "./modules/task-queue.js";
8
- import { MessageRouter } from "./modules/message-router.js";
9
- import { MessageWatcher } from "./modules/message-watcher.js";
10
- import { ContextStore } from "./modules/context-store.js";
11
- import { TaskPriorityName, } from "./models/schemas.js";
12
- // ── Start server ──
13
- export async function startServer(config) {
14
- const zk = new ZkClient(config.zkHosts);
15
- await zk.connect();
16
- const registry = new InstanceRegistry(zk);
17
- const taskQueue = new TaskQueue(zk);
18
- const messageRouter = new MessageRouter(zk);
19
- const messageWatcher = new MessageWatcher(zk);
20
- const contextStore = new ContextStore(zk);
21
- // ── Setup MCP Server ──
22
- const mcp = new McpServer({
23
- name: "ClaudeMCP",
24
- version: "0.2.1",
25
- }, {
26
- capabilities: {
27
- resources: { subscribe: true },
28
- },
29
- });
30
- // ── Register Tools (18) ──
31
- // 1. server_status
32
- mcp.tool("server_status", async () => {
33
- const zkOk = zk.connected;
34
- return {
35
- content: [
36
- {
37
- type: "text",
38
- text: `Server: running\nZooKeeper: ${zkOk ? "connected" : "DISCONNECTED"}\nPort: ${config.port}`,
39
- },
40
- ],
41
- };
42
- });
43
- // 2. register_instance
44
- mcp.tool("register_instance", {
45
- name: z.string().min(1),
46
- role: z.string().default("general"),
47
- instance_id: z.string().optional(),
48
- work_dir: z.string().optional(),
49
- }, async ({ name, role, instance_id, work_dir }) => {
50
- if (!zk.connected) {
51
- return { content: [{ type: "text", text: "Error: ZooKeeper is not connected." }] };
52
- }
53
- const instance = await registry.register(name, role, instance_id);
54
- const action = instance_id && instance.id === instance_id ? "re-registered" : "registered";
55
- let watcherNote = "";
56
- if (work_dir) {
57
- await messageWatcher.startWatching(instance.id, work_dir);
58
- watcherNote = `\nMessage watcher started for: ${work_dir}`;
59
- }
60
- return {
61
- content: [
62
- {
63
- type: "text",
64
- text: `Instance ${action}:\n${JSON.stringify(instance, null, 2)}\n\n` +
65
- `To receive real-time messages, subscribe to: orchestrator://messages/${instance.id}${watcherNote}`,
66
- },
67
- ],
68
- };
69
- });
70
- // 3. heartbeat
71
- mcp.tool("heartbeat", {
72
- instance_id: z.string(),
73
- current_task: z.string().optional(),
74
- }, async ({ instance_id, current_task }) => {
75
- await registry.heartbeat(instance_id, current_task);
76
- return { content: [{ type: "text", text: "ok" }] };
77
- });
78
- // 4. list_instances
79
- mcp.tool("list_instances", async () => {
80
- const instances = await registry.listAll();
81
- const n = instances.length;
82
- const header = `${n} active instance${n !== 1 ? "s" : ""}:\n`;
83
- const lines = instances.map((i) => ` [${i.role}] ${i.name} (${i.id.slice(0, 8)}...) status=${i.status}`);
84
- return { content: [{ type: "text", text: header + lines.join("\n") }] };
85
- });
86
- // 5. push_task
87
- mcp.tool("push_task", {
88
- title: z.string().min(1),
89
- description: z.string().default(""),
90
- priority: z.number().int().min(0).max(2).default(1),
91
- instance_id: z.string().default(""),
92
- assignee: z.string().optional(),
93
- }, async ({ title, description, priority, instance_id, assignee }) => {
94
- const task = await taskQueue.push(title, description, priority, instance_id, assignee);
95
- const prioName = TaskPriorityName[task.priority] ?? "MEDIUM";
96
- return {
97
- content: [
98
- {
99
- type: "text",
100
- text: `Task ${task.id} created:\n title: ${task.title}\n priority: ${prioName}`,
101
- },
102
- ],
103
- };
104
- });
105
- // 6. claim_task
106
- mcp.tool("claim_task", { instance_id: z.string() }, async ({ instance_id }) => {
107
- const task = await taskQueue.claim(instance_id);
108
- if (!task) {
109
- return { content: [{ type: "text", text: "No pending tasks available." }] };
110
- }
111
- return {
112
- content: [
113
- {
114
- type: "text",
115
- text: `Claimed task ${task.id}\n title: ${task.title}\n description: ${task.description}`,
116
- },
117
- ],
118
- };
119
- });
120
- // 7. complete_task
121
- mcp.tool("complete_task", {
122
- instance_id: z.string(),
123
- task_id: z.string(),
124
- result: z.string(),
125
- }, async ({ instance_id, task_id, result: resultText }) => {
126
- const task = await taskQueue.complete(instance_id, task_id, resultText);
127
- return { content: [{ type: "text", text: `Task ${task.id} completed.` }] };
128
- });
129
- // 8. list_tasks
130
- mcp.tool("list_tasks", { status: z.string().optional() }, async ({ status }) => {
131
- const tasks = await taskQueue.listTasks(status);
132
- if (tasks.length === 0) {
133
- return { content: [{ type: "text", text: "No tasks found." }] };
134
- }
135
- const lines = [`${tasks.length} task(s):`];
136
- for (const t of tasks) {
137
- lines.push(` [${t.status}] ${t.id}: ${t.title || "(no title)"}`);
138
- }
139
- return { content: [{ type: "text", text: lines.join("\n") }] };
140
- });
141
- // 9. send_message
142
- mcp.tool("send_message", {
143
- instance_id: z.string(),
144
- content: z.string().min(1),
145
- to_instance: z.string().optional(),
146
- to_name: z.string().optional(),
147
- broadcast: z.boolean().default(false),
148
- }, async ({ instance_id, content, to_instance, to_name, broadcast }) => {
149
- const inst = await registry.get(instance_id);
150
- const fromName = inst?.name ?? instance_id.slice(0, 8);
151
- const messages = await messageRouter.send(instance_id, fromName, content, to_instance, broadcast, to_name);
152
- // Push notifications to targets so subscribed clients get real-time delivery
153
- const targets = messages.map((m) => m.to_instance);
154
- await Promise.allSettled(targets.map((tid) => mcp.server.sendResourceUpdated({ uri: `orchestrator://messages/${tid}` })));
155
- return {
156
- content: [{ type: "text", text: `Message sent to: ${JSON.stringify(targets)}` }],
157
- };
158
- });
159
- // 10. poll_messages
160
- mcp.tool("poll_messages", { instance_id: z.string() }, async ({ instance_id }) => {
161
- const messages = await messageRouter.poll(instance_id);
162
- if (messages.length === 0) {
163
- return { content: [{ type: "text", text: "No messages." }] };
164
- }
165
- const lines = [`${messages.length} message(s):`];
166
- for (const m of messages) {
167
- const readMark = m.read ? " [read]" : "";
168
- lines.push(` [${m.type}] from ${m.from_name}: ${m.content.slice(0, 100)}${readMark}`);
169
- }
170
- return { content: [{ type: "text", text: lines.join("\n") }] };
171
- });
172
- // 11. wait_for_message
173
- mcp.tool("wait_for_message", {
174
- instance_id: z.string(),
175
- timeout_seconds: z.number().int().min(1).default(30),
176
- }, async ({ instance_id, timeout_seconds }) => {
177
- const messages = await messageRouter.waitForMessage(instance_id, timeout_seconds);
178
- if (messages.length === 0) {
179
- return { content: [{ type: "text", text: "No messages received within timeout." }] };
180
- }
181
- const lines = [`${messages.length} message(s):`];
182
- for (const m of messages) {
183
- lines.push(` [${m.type}] from ${m.from_name}: ${m.content.slice(0, 200)}`);
184
- }
185
- return { content: [{ type: "text", text: lines.join("\n") }] };
186
- });
187
- // 12. mark_read
188
- mcp.tool("mark_read", {
189
- instance_id: z.string(),
190
- message_id: z.string(),
191
- }, async ({ instance_id, message_id }) => {
192
- await messageRouter.markRead(instance_id, message_id);
193
- return { content: [{ type: "text", text: `Message ${message_id} marked as read.` }] };
194
- });
195
- // 13. dismiss_message
196
- mcp.tool("dismiss_message", {
197
- instance_id: z.string(),
198
- message_id: z.string(),
199
- }, async ({ instance_id, message_id }) => {
200
- await messageRouter.dismissMessage(instance_id, message_id);
201
- return { content: [{ type: "text", text: `Message ${message_id} dismissed.` }] };
202
- });
203
- // 14. request_help
204
- mcp.tool("request_help", {
205
- instance_id: z.string(),
206
- question: z.string().min(1),
207
- context: z.string().optional(),
208
- }, async ({ instance_id, question, context }) => {
209
- const inst = await registry.get(instance_id);
210
- const fromName = inst?.name ?? instance_id.slice(0, 8);
211
- const messages = await messageRouter.requestHelp(instance_id, fromName, question, context);
212
- const targets = messages.map((m) => m.to_instance);
213
- await Promise.allSettled(targets.map((tid) => mcp.server.sendResourceUpdated({ uri: `orchestrator://messages/${tid}` })));
214
- return {
215
- content: [
216
- {
217
- type: "text",
218
- text: `Help request broadcast to ${targets.length} instance(s): ${JSON.stringify(targets)}`,
219
- },
220
- ],
221
- };
222
- });
223
- // 15. set_context
224
- mcp.tool("set_context", {
225
- key: z.string().min(1),
226
- value: z.string(),
227
- instance_id: z.string().default(""),
228
- }, async ({ key, value, instance_id }) => {
229
- await contextStore.set(key, value, instance_id);
230
- return { content: [{ type: "text", text: `Context set: ${key} = ${value}` }] };
231
- });
232
- // 16. get_context
233
- mcp.tool("get_context", { key: z.string() }, async ({ key }) => {
234
- const value = await contextStore.get(key);
235
- if (value === null) {
236
- return { content: [{ type: "text", text: `No context found for key: ${key}` }] };
237
- }
238
- return { content: [{ type: "text", text: `${key} = ${value}` }] };
239
- });
240
- // 17. delete_context
241
- mcp.tool("delete_context", { key: z.string() }, async ({ key }) => {
242
- await contextStore.delete(key);
243
- return { content: [{ type: "text", text: `Context deleted: ${key}` }] };
244
- });
245
- // 18. list_context_keys
246
- mcp.tool("list_context_keys", async () => {
247
- const keys = await contextStore.listKeys();
248
- if (keys.length === 0) {
249
- return { content: [{ type: "text", text: "No context keys found." }] };
250
- }
251
- return { content: [{ type: "text", text: `${keys.length} key(s):\n${keys.join("\n")}` }] };
252
- });
253
- // ── Register Resources (5) ──
254
- // 1. orchestrator://instances
255
- mcp.resource("instances", "orchestrator://instances", { description: "All active instances" }, async () => {
256
- const instances = await registry.listAll();
257
- return {
258
- contents: [
259
- {
260
- uri: "orchestrator://instances",
261
- mimeType: "application/json",
262
- text: JSON.stringify(instances, null, 2),
263
- },
264
- ],
265
- };
266
- });
267
- // 2. orchestrator://tasks/pending
268
- mcp.resource("tasks-pending", "orchestrator://tasks/pending", { description: "Pending tasks" }, async () => {
269
- const tasks = await taskQueue.listTasks("pending");
270
- return {
271
- contents: [
272
- {
273
- uri: "orchestrator://tasks/pending",
274
- mimeType: "application/json",
275
- text: JSON.stringify(tasks, null, 2),
276
- },
277
- ],
278
- };
279
- });
280
- // 3. orchestrator://tasks/in-progress
281
- mcp.resource("tasks-in-progress", "orchestrator://tasks/in-progress", { description: "Claimed tasks in progress" }, async () => {
282
- const tasks = await taskQueue.listTasks("claimed");
283
- return {
284
- contents: [
285
- {
286
- uri: "orchestrator://tasks/in-progress",
287
- mimeType: "application/json",
288
- text: JSON.stringify(tasks, null, 2),
289
- },
290
- ],
291
- };
292
- });
293
- // 4. orchestrator://messages/{instance_id} (supports subscription)
294
- mcp.resource("messages", new ResourceTemplate("orchestrator://messages/{instance_id}", {
295
- list: undefined,
296
- }), { description: "Messages for a specific instance" }, async (uri, variables) => {
297
- const instanceId = variables.instance_id ?? "";
298
- const messages = await messageRouter.poll(instanceId);
299
- return {
300
- contents: [
301
- {
302
- uri: uri.href,
303
- mimeType: "application/json",
304
- text: JSON.stringify(messages, null, 2),
305
- },
306
- ],
307
- };
308
- });
309
- // 5. orchestrator://context/{key} (supports subscription)
310
- mcp.resource("context", new ResourceTemplate("orchestrator://context/{key}", {
311
- list: undefined,
312
- }), { description: "Shared context value by key" }, async (uri, variables) => {
313
- const key = variables.key ?? "";
314
- const value = await contextStore.get(key);
315
- return {
316
- contents: [
317
- {
318
- uri: uri.href,
319
- mimeType: "application/json",
320
- text: JSON.stringify(value !== null ? { key, value } : { key, value: null }, null, 2),
321
- },
322
- ],
323
- };
324
- });
325
- // ── Register Prompts (2) ──
326
- // 1. orchestrate-task
327
- mcp.prompt("orchestrate-task", "Help break down a goal into assignable tasks", {
328
- goal: z.string().describe("The goal or objective to break down"),
329
- context: z.string().optional().describe("Additional context about team state"),
330
- }, async ({ goal, context: ctx }) => {
331
- const instances = await registry.listAll();
332
- const pendingTasks = await taskQueue.listTasks("pending");
333
- const members = instances
334
- .map((i) => `- ${i.name} (${i.id.slice(0, 8)}...) [${i.role}] status=${i.status}`)
335
- .join("\n");
336
- return {
337
- messages: [
338
- {
339
- role: "user",
340
- content: {
341
- type: "text",
342
- text: `Goal: ${goal}\n\n` +
343
- `Team members:\n${members}\n\n` +
344
- `Pending tasks: ${pendingTasks.length}\n` +
345
- (ctx ? `Additional context: ${ctx}\n\n` : "") +
346
- `Please break this goal down into specific, assignable tasks. For each task, suggest:\n` +
347
- `1. A clear title\n` +
348
- `2. A brief description\n` +
349
- `3. Priority (HIGH=0, MEDIUM=1, LOW=2)\n` +
350
- `4. Which team member should handle it\n\n` +
351
- `Output the tasks in a format ready to assign.`,
352
- },
353
- },
354
- ],
355
- };
356
- });
357
- // 2. team-status
358
- mcp.prompt("team-status", "Summarize current team state", async () => {
359
- const instances = await registry.listAll();
360
- const pending = await taskQueue.listTasks("pending");
361
- const claimed = await taskQueue.listTasks("claimed");
362
- const members = instances
363
- .map((i) => `- ${i.name} (${i.id.slice(0, 8)}...) [${i.role}] status=${i.status}`)
364
- .join("\n");
365
- const pendingList = pending
366
- .map((t) => `- [${TaskPriorityName[t.priority]}] ${t.title} (${t.id})`)
367
- .join("\n");
368
- const claimedList = claimed
369
- .map((t) => `- ${t.title} by ${t.claimed_by?.slice(0, 8)}... (${t.id})`)
370
- .join("\n");
371
- return {
372
- messages: [
373
- {
374
- role: "user",
375
- content: {
376
- type: "text",
377
- text: `# Team Status\n\n` +
378
- `## Members (${instances.length})\n${members}\n\n` +
379
- `## Pending Tasks (${pending.length})\n${pendingList || "(none)"}\n\n` +
380
- `## In Progress (${claimed.length})\n${claimedList || "(none)"}`,
381
- },
382
- },
383
- ],
384
- };
385
- });
386
- // ── Start HTTP server with MCP transport ──
387
- const app = express();
388
- app.use(express.json());
389
- const transport = new StreamableHTTPServerTransport({
390
- sessionIdGenerator: undefined, // stateless mode
391
- });
392
- // Connect MCP server to transport
393
- await mcp.connect(transport);
394
- // MCP endpoint
395
- app.post("/mcp", async (req, res) => {
396
- try {
397
- await transport.handleRequest(req, res, req.body);
398
- }
399
- catch (err) {
400
- if (!res.headersSent) {
401
- res.status(500).json({ error: "MCP request handling failed" });
402
- }
403
- }
404
- });
405
- app.get("/mcp", async (req, res) => {
406
- try {
407
- await transport.handleRequest(req, res);
408
- }
409
- catch (err) {
410
- if (!res.headersSent) {
411
- res.status(500).json({ error: "MCP request handling failed" });
412
- }
413
- }
414
- });
415
- app.delete("/mcp", async (req, res) => {
416
- try {
417
- await transport.handleRequest(req, res);
418
- }
419
- catch (err) {
420
- if (!res.headersSent) {
421
- res.status(500).json({ error: "MCP request handling failed" });
422
- }
423
- }
424
- });
425
- // REST endpoint for CLI-based registration (uses server's persistent ZK connection)
426
- app.post("/register", async (req, res) => {
427
- try {
428
- const { name, role, instance_id, work_dir } = req.body;
429
- if (!name || typeof name !== "string") {
430
- res.status(400).json({ error: "name is required" });
431
- return;
432
- }
433
- if (!zk.connected) {
434
- res.status(503).json({ error: "ZooKeeper is not connected" });
435
- return;
436
- }
437
- const instance = await registry.register(name, typeof role === "string" ? role : "general", typeof instance_id === "string" ? instance_id : undefined);
438
- if (typeof work_dir === "string" && work_dir.length > 0) {
439
- await messageWatcher.startWatching(instance.id, work_dir);
440
- }
441
- res.json(instance);
442
- }
443
- catch (err) {
444
- res.status(500).json({ error: String(err) });
445
- }
446
- });
447
- // REST endpoint for CLI-based unregistration
448
- app.post("/unregister", async (req, res) => {
449
- try {
450
- const { instance_id } = req.body;
451
- if (!instance_id || typeof instance_id !== "string") {
452
- res.status(400).json({ error: "instance_id is required" });
453
- return;
454
- }
455
- messageWatcher.stopWatching(instance_id);
456
- if (!zk.connected) {
457
- res.json({ status: "unregistered", instance_id });
458
- return;
459
- }
460
- await registry.unregister(instance_id);
461
- res.json({ status: "unregistered", instance_id });
462
- }
463
- catch (err) {
464
- res.status(500).json({ error: String(err) });
465
- }
466
- });
467
- // Health check
468
- app.get("/health", (_req, res) => {
469
- res.json({ status: "ok", zookeeper: zk.connected ? "connected" : "disconnected" });
470
- });
471
- // ── Shutdown handlers ──
472
- const shutdown = async (signal) => {
473
- console.log(`\n[server] Received ${signal}, shutting down...`);
474
- messageWatcher.stopAll();
475
- await zk.disconnect();
476
- process.exit(0);
477
- };
478
- process.on("SIGTERM", () => shutdown("SIGTERM"));
479
- process.on("SIGINT", () => shutdown("SIGINT"));
480
- return new Promise((resolve) => {
481
- app.listen(config.port, config.host, () => {
482
- console.log(`MCP server listening on ${config.host}:${config.port}`);
483
- console.log(`MCP endpoint: http://${config.host}:${config.port}/mcp`);
484
- console.log(`Register CLI: http://${config.host}:${config.port}/register`);
485
- console.log(`Health check: http://${config.host}:${config.port}/health`);
486
- resolve();
487
- });
488
- });
489
- }
490
- //# sourceMappingURL=server.js.map